.Net组件程序设计之线程、并发管理(二)
2.同步线程
手动同步
监视器
互斥
可等待事件
同步线程
所有的.NET组件都支持在多线程的环境中运行,可以被多个线程并发访问,如果没有线程同步,这样的后果是当多个线程同时访问 对象状态时,对象的状态可能被破坏,造成不一致性。.NET提供了两种方法来避免这样的问题,使得我们设计的组件更加健壮。 第一种是自动同步,让你使用一个属性来修饰组件,这样就可以把组件交给.NET了,同步的事情也就交给了.NET。 第二种是手动同步,这是让你使用.NET提供的同步对象来实现线程同步,也不是太复杂,本篇将会对手动同步来稍作讲解。
2.1 手动同步
.NET手动同步提供了一套丰富的同步锁,上一节说到同步域,同步域事实上是一个巨大的宏锁,而手动同步则提供了 对被锁对象的细粒度控制,可以控制访问对象、单一成员甚至是单行的代码。这样的好处就是有可能的提高系统的性能和吞吐量。
2.1.1 监视器
监视器是一种只能和引用类型一块工作的锁。
2.1.1-1
1
2
3
4
5
6
7
8
9
10
|
1
public
class
ManualSynchronization
2 {
3
public
void
DoSomeThing()
4 {
5
for
(
int
i = 0; i < 100; i++)
6 {
7 Console.WriteLine(i.ToString());
8 }
9 }
10 }
|
1
2
3
4
5
6
7
8
9
10
11
|
1 ManualSynchronization monitorcase =
new
ManualSynchronization();
2
3 Monitor.Enter(monitorcase);
4
try
5 {
6 monitorcase.DoSomeThing();
7 }
8
finally
9 {
10 Monitor.Exit(monitorcase);
11 }
|
任何线程的任何对象都可以调用Enter()方法来锁定对象,如果Monitor正在被一个线程使用,而这个时候又有一个线程来请求对象Enter(),这样就会使第二个线程阻塞,直到第一个线程调用Exit(),如果这时有多个线程请求对象Enter(),它们就会被放置在一个叫做锁队列的队列里,并依照队列的顺序获得服务顺序。
你还可以使用Monitor类为静态类方法或静态属性提供安全线程访问,方法是让Monitor锁定该类型,而不是一个实例:
2.1.1-2
1
2
3
4
5
6
7
8
9
10
|
1
public
class
ManualSynchronization
2 {
3
public
static
void
SDoSomeThing()
4 {
5
for
(
int
i = 0; i < 100; i++)
6 {
7 Console.WriteLine(i.ToString());
8 }
9 }
10 }
|
1
2
3
4
5
6
7
8
9
|
1 Monitor.Enter(
typeof
(ManualSynchronization));
2
try
3 {
4 ManualSynchronization.SDoSomeThing();
5 }
6
finally
7 {
8 Monitor.Exit(
typeof
(ManualSynchronization));
9 }
|
在C#中为了简化这样的写法,提供了lock语句,使编译器在try/finally语句中自动产生对Enter()和Exit()的调用。
比如你写下这样的代码等同于2.1.1-1的示例代码:
2.1.1-3
1
2
3
4
5
|
1 ManualSynchronization monitorcase =
new
ManualSynchronization();
2
lock
(monitorcase)
3 {
4 monitorcase.DoSomeThing();
5 }
|
像上面的代码这样写看似没什么问题了,因为这个lock所定对象实例或者是对象类型,是根据客户端开发者的判断而定的,这样的锁定方式与客户端耦合度大,看下以下代码:
2.1.1-4
1
2
3
4
5
6
7
8
9
10
11
12
13
|
1
public
class
ManualSynchronization
2 {
3
public
void
DoSomeThing()
4 {
5
lock
(
this
)
6 {
7
for
(
int
i = 0; i < 100; i++)
8 {
9 Console.WriteLine(i.ToString());
10 }
11 }
12 }
13 }
|
1
2
|
1 ManualSynchronization monitorcase =
new
ManualSynchronization();
2 monitorcase.DoSomeThing();
|
这样感觉是不是舒服不少,这就是方法同步了,.NET内部也对它提供了支持,定义在System.Runtime.CompilerServices命名空间里的MethodImpl方法属性接受一个MethodImplOptions类型的枚举。其中一个枚举值是MethodImplOptions.Synchronized。当运行这个枚举值的时候,编辑器就指示.NET运行时在方法入口锁定对象,语义和2.1.1-4的代码断相同:
2.1.1-5
1
2
3
4
5
6
7
8
|
1
public
class
ManualSynchronization
2 {
3 [MethodImpl( MethodImplOptions.Synchronized)]
4
public
void
DoSomeThingSynchroniezd()
5 {
6 Console.WriteLine(
"studycase"
);
7 }
8 }
|
2.1.2 互斥
这一个小节要讲到的是Mutex类,它是从WaitHandle派生的类,它保证了各个线程在某个资源或代码块上相互排斥。
2.1.2-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
1
public
class
MutexDom:IDisposable
2 {
3
public
MutexDom(){}
4
private
int
_Num = 0;
5
public
int
Num
6 {
7
get
8 {
9
return
_Num;
10 }
11
set
12 {
13 _Num = value;
14 }
15 }
16
public
void
Dom()
17 {
18
for
(
int
i = 0; i < 100; i++)
19 {
20 Num = Num + i;
21 Console.WriteLine(Thread.CurrentThread.Name +
"_"
+ Num.ToString() +
"_"
+Thread.CurrentThread.ManagedThreadId.ToString());
22 }
23
24
25 }
26
public
void
Dispose()
27 {
28
29 }
30
31
public
static
void
Test()
32 {
33 MutexDom mutexDom=
new
MutexDom();
34 ThreadStart threadStart=
new
ThreadStart(mutexDom.Dom);
35 Thread thread1 =
new
Thread(threadStart);
36 thread1.Name =
"Thread_One"
;
37 Thread thread2 =
new
Thread(threadStart);
38 thread2.Name =
"Thread_Two"
;
39 thread1.Start();
40 thread2.Start();
41 }
42 }
|
MutexDom.Test();启动测试,我所希望的效果是Dom()方法是有序的执行的,而我用了一个int类型的Nun属性来作为计数器,那我们就一起来看一下结果吧(可能每次运行结果不一样)
我所期望的在线程Thread_One中执行0递增至99的值时4950,而在结果中已经超出了这个范围,这说明了什么?说明了两个线程在交替的对Num进行操作。修改一下代码,再来看一下:
2.1.2-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
1
public
class
MutexDom:IDisposable
2 {
3
private
Mutex _Mutex;
4
public
MutexDom()
5 {
6 _Mutex =
new
Mutex();
7 }
8
private
int
_Num = 0;
9
public
int
Num
10 {
11
get
12 {
13
return
_Num;
14 }
15
set
16 {
17 _Num = value;
18 }
19 }
20
public
void
Dom()
21 {
22 _Mutex.WaitOne();
//如果当前资源被占用 则等待占用它的线程发送消息
23
try
24 {
25
for
(
int
i = 0; i < 100; i++)
26 {
27 Num = Num + i;
28 Console.WriteLine(Thread.CurrentThread.Name +
"_"
+ Num.ToString() +
"_"
+Thread.CurrentThread.ManagedThreadId.ToString());
29 }
30 }
31
finally
32 {
33 _Mutex.ReleaseMutex();
34 }
35
36 }
37
public
void
Dispose()
38 {
39 _Mutex.Close();
40 }
41
42
public
static
void
Test()
43 {
44 MutexDom mutexDom=
new
MutexDom();
45 ThreadStart threadStart=
new
ThreadStart(mutexDom.Dom);
46 Thread thread1 =
new
Thread(threadStart);
47 thread1.Name =
"Thread_One"
;
48 Thread thread2 =
new
Thread(threadStart);
49 thread2.Name =
"Thread_Two"
;
50
51 thread1.Start();
52 thread2.Start();
53
54 }
55 }
|
从结果中得出,是线程Thread_Two先执行的,这个没关系,只要看它的结果值就行了,这就说明了,在线程"Thread_Two"执行对Dom()方法操作的时候"Thread_One"是肯定已经启动了的,而且是在等待"Thread_Two"的释放消息,这样就保持了对象状态的一致性,这个时候"Thread_One"是在一个等待队列中的。如果这个时候"Thread_One"调用ReleaseMutex()方法,是会报错的,因为ReleaseMutex()方法是只能当前所占有的线程来进行释放,互斥就这样完成了。
2.1.3 可等待事件
EventWaitHandle类派生于WaitHandle,被用于跨线程通知事件。 它有两种状态:信号已发状态、信号未发状态。 Set()方法和 Reset()方法分别把句柄状态设置为信号已发或信号未发。 它有两种使用方式,一种是手动重置,还有一种是自动重置。是通过给构造函数提供一个EventResetMode类型的枚举值,
1
2
3
4
5
|
1
public
enum
EventResetMode
2 {
3 AutoReset,
4 ManualReset
5 }
|
.NET提供了EventWaitHandle的两个强类型子类,定义如下:
1
2
3
4
5
6
7
8
9
10
|
1
public
class
ManualResetEvent:EventWaitHandle
2 {
3
public
ManualResetEvent(
bool
initialState):
base
(initialState,EventResetMode.ManualReset)
4 {}
5 }
6
public
sealed
class
AutoResetEvent : EventWaitHandle
7 {
8
public
AutoResetEvent(
bool
initialState):
base
(initialState,EventResetMode.AutoReset)
9 {}
10 }
|
先来看一下手动重置:
2.1.3-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
1
public
class
EventDom:IDisposable
2 {
3 ManualResetEvent _WaitHandle;
4
public
EventDom()
5 {
6 _WaitHandle =
new
ManualResetEvent(
true
);
7
8 Thread thread =
new
Thread(DoWork);
9 thread.Start();
10 }
11
private
void
DoWork()
12 {
13
int
num = 0;
14
while
(
true
)
15 {
16 _WaitHandle.WaitOne();
17 num++;
18 Console.WriteLine(
"EventDom_"
+ num.ToString());
19 }
20 }
21
public
void
StartThread()
22 {
23 _WaitHandle.Set();
24 Console.WriteLine(
"EventDom->StartThread"
);
25 }
26
public
void
StopThread()
27 {
28 _WaitHandle.Reset();
29 Console.WriteLine(
"EventDom->StopThread"
);
30 }
31
public
void
Dispose()
32 {
33 _WaitHandle.Close();
34 }
35
36
public
static
void
Test()
37 {
38 EventDom eventDom =
new
EventDom();
39 eventDom.StopThread();
40 }
41
42 }
|
调用EventDom.Test();进行测试,结果如下图:
在构造函数中我就已经把手动重置事件声明为了 信号已发状态,所以在运行的时候,while在每次循环的时候等待接收到的信号一直都是已发送状态,所以是一直在输出,直到调用了StopThread()方法中的Reset()方法,把状态设置为未发送状态,才使执行暂停。
再来看一下自动重置,修改一下上段的代码,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
1
public
class
EventDom : IDisposable
2 {
3 AutoResetEvent _WaitHandle;
4
public
EventDom()
5 {
6 _WaitHandle =
new
AutoResetEvent(
true
);
7
8 Thread thread =
new
Thread(DoWork);
9 thread.Start();
10 }
11
private
void
DoWork()
12 {
13
int
num = 0;
14
while
(
true
)
15 {
16 _WaitHandle.WaitOne();
17 num++;
18 Console.WriteLine(
"EventDom_"
+ num.ToString());
19 }
20 }
21
public
void
StartThread()
22 {
23 _WaitHandle.Set();
24 Console.WriteLine(
"EventDom->StartThread"
);
25 }
26
public
void
StopThread()
27 {
28 _WaitHandle.Reset();
29 Console.WriteLine(
"EventDom->StopThread"
);
30 }
31
public
void
Dispose()
32 {
33 _WaitHandle.Close();
34 }
35
36
public
static
void
Test()
37 {
38 EventDom eventDom =
new
EventDom();
39 eventDom.StartThread();
40 }
41 }
|
首先把手动重置类型换成了自动重置类型,然后再测试代码中把设置状态为未发送的方法,改成了设置状态为已发送的方法。
这个结果是正确,因为自动重置类型就是事件状态被设置为信号已发,它就会保持这个状态,直到某个线程从等待调用中释放出来,然后在这个时候,它的状态会发生改变,自动的反转到未发送状态。
还有一些扩展的知识点就不在这一一阐述了,希望本篇能对大家有所帮助。END
本文转自jinyuan0829 51CTO博客,原文链接:http://blog.51cto.com/jinyuan/1421966,如需转载请自行联系原作者