最近由于在准备 如果在同一个
Collection
对象培训的
PPT
,因为涉及到
SyncRoot
的属性的讲解,所以对怎样在多线程应用程序中同步资源访
问
做了个总结:
对于引用类型和非线程安全的资源的同步处理,有四种相关处理:
lock
关键字,监视器
(
Monitor
),
同
步
事件和等待句
柄
,
mutex
类。
Lock
关键字
本人愚钝
,在以前编程中遇到
lock的问题总是使用
lock(this)一锁了之,出问题后翻看
MSDN突然发现下面几行字:
通常,
应
避免
锁
定
public 类
型,否
则实
例将超出代
码
的控制范
围
。常
见
的
结
构
lock (this)、
lock (typeof (MyType)) 和
lock ("myLock") 违
反此准
则
:
如果
实
例可以被公共
访问
,将出
现
lock (this) 问题
。
如果
MyType 可以被公共
访问
,将出
现
lock (typeof (MyType)) 问题
。
由于
进
程中使用同一字符串的任何其他代
码
将共享同一个
锁
,所以出
现
lock(“myLock”) 问题。
来
看
看
lock(this)的
问题
:如果有一个类
Class1,该类有一个方法用
lock(this)来实现互斥:
void
Method2()
{
lock
(
this
)
{
System.Windows.Forms.MessageBox.Show(
"
Method2 End
"
);
}
}
Class1
的实例中
,
该
Method2
能够互斥的执行。但是如果是
2
个
Class1
的实例分别来执行
Method2,
是没有互斥效果的。因为这里的
lock,
只是对当前的实例对象进行了加锁。
Lock(typeof(MyType))锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类型对象(该对象是
typeof的返回结果),锁定它,就锁定了该对象的所有实例,微软现在建议(原文请参考:
http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/SDaskgui06032003.mspx?mfr=true
)不要使用
lock(typeof(MyType)),
因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。
锁住一个字符串更为神奇,只要字符串内容相同,就能引起程序挂起。原因是在
.NET
中,字符串会被暂时存放,如果两个变量的字符串内容相同的话,
.NET
会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用
lock(“my lock”)
的话,它们实际锁住的是同一个对象。
到此,微软给出了个
lock的建议用法:锁定一个私有的
static 成员变量。
.NET
在一些集合类中(比如
ArrayList,HashTable
,
Queue
,
Stack
)已经提供了一个供
lock
使用的对象
SyncRoot
,用
Reflector
工具查看了
SyncRoot
属性的代码,在
Array
中,该属性只有一句话
:return this,
这样和
lock array
的当前实例是一样的。
ArrayList
中的
SyncRoot
有所不同
get
{
if
(
this
._syncRoot
==
null
)
{
Interlocked.CompareExchange(
ref
this
._syncRoot,
new
object
(),
null
);
}
return
this
._syncRoot;
其中
Interlocked
类是专门为多个线程共享的变量提供原子操作(如果你想锁定的对象是基本数据类型,那么请使用这个类),
CompareExchange
方法将当前
syncRoot
和
null
做比较,如果相等,就替换成
new object()
,这样做是为了保证多个线程在使用
syncRoot
时是线程安全的。集合类中还有一个方法是和同步相关的:
Synchronized
,该方法返回一个对应的集合类的
wrapper
类,该类是线程安全的,因为他的大部分方法都用
lock
来进行了同步处理,比如
Add
方法:
override
void
Add(
object
key,
object
value)
{
lock
(
this
._table.SyncRoot)
{
this
._table.Add(key, value);
}
}
这里要特别注意的是
MSDN
提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合:
=
new
Queue();
lock
(myCollection.SyncRoot) {
foreach
(Object item
in
myCollection) {
//
Insert your code here.
}
}
Monitor
类
该类功效和
lock
类似:
=
(System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
执行结果如下(有可能是ThreadPong先执行):
lock关键字比
Monitor简洁,其实
lock就是对
Monitor的
Enter和
Exit的一个封装。另外
Monitor还有几个常用的方法:
TryEnter能够有效的决绝长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用
TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间
bool gotLock = Monitor.TryEnter(
myobject,1000)
,让当前线程在等待
1000秒后根据返回的
bool值来决定是否继续下面的操作。Pulse以及PulseAll还有Wait方法是成对使用的,它们能让你更精确的控制线程之间的并发,MSDN关于这3个方法的解释很含糊,有必要用一个具体的例子来说明一下:
System.Threading;
public
class
Program {
static
object
ball
=
new
object
();
public
static
void
Main() {
Thread threadPing
=
new
Thread( ThreadPingProc );
Thread threadPong
=
new
Thread( ThreadPongProc );
threadPing.Start(); threadPong.Start();
}
static
void
ThreadPongProc() {
System.Console.WriteLine(
"
ThreadPong: Hello!
"
);
lock
( ball )
for
(
int
i
=
0
; i
<
5
; i
++
){
System.Console.WriteLine(
"
ThreadPong: Pong
"
);
Monitor.Pulse( ball );
Monitor.Wait( ball );
}
System.Console.WriteLine(
"
ThreadPong: Bye!
"
);
}
static
void
ThreadPingProc() {
System.Console.WriteLine(
"
ThreadPing: Hello!
"
);
lock
( ball )
for
(
int
i
=
0
; i
<
5
; i
++
){
System.Console.WriteLine(
"
ThreadPing: Ping
"
);
Monitor.Pulse( ball );
Monitor.Wait( ball );
}
System.Console.WriteLine(
"
ThreadPing: Bye!
"
);
}
}
!
ThreadPing: Ping
ThreadPong: Hello
!
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Bye
!
当threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball
);后,它通知threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball
)阻塞自己后,它放弃了了对ball的锁定,所以threadPong得以执行。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进
程发送通知信号,如果只有一个线程被阻塞,那么请使用Pulse方法。
同步事件和等待句柄
同步事件和等待句柄用于解决更复杂的同步情况,比如一个一个大的计算步骤包含3个步骤result = first term +
second term + third term,如果现在想写个多线程程序,同时计算first term,second term 和third
term,等所有3个步骤计算好后再把它们汇总起来,我们就需要使用到同步事件和等待句柄,同步事件分有两个,分别为AutoResetEvent和
ManualResetEvent,这两个类可以用来代表某个线程的运行状态:终止和非终止,等待句柄用来判断ResetEvent的状态,如果是非终止
状态就一直等待,否则放行,让等待句柄下面的代码继续运行。下面的代码示例阐释了如何使用等待句柄来发送复杂数字计算的不同阶段的完成信号。此计算的格式
为:result = first term + second term + third term
System;
using
System.Threading;
class
CalculateTest
{
static
void
Main()
{
Calculate calc
=
new
Calculate();
Console.WriteLine(
"
Result = {0}.
"
,
calc.Result(
234
).ToString());
Console.WriteLine(
"
Result = {0}.
"
,
calc.Result(
55
).ToString());
}
}
class
Calculate
{
double
baseNumber, firstTerm, secondTerm, thirdTerm;
AutoResetEvent[] autoEvents;
ManualResetEvent manualEvent;
//
Generate random numbers to simulate the actual calculations.
Random randomGenerator;
public
Calculate()
{
autoEvents
=
new
AutoResetEvent[]
{
new
AutoResetEvent(
false
),
new
AutoResetEvent(
false
),
new
AutoResetEvent(
false
)
};
manualEvent
=
new
ManualResetEvent(
false
);
}
void
CalculateBase(
object
stateInfo)
{
baseNumber
=
randomGenerator.NextDouble();
//
Signal that baseNumber is ready.
manualEvent.Set();
}
//
The following CalculateX methods all perform the same
//
series of steps as commented in CalculateFirstTerm.
void
CalculateFirstTerm(
object
stateInfo)
{
//
Perform a precalculation.
double
preCalc
=
randomGenerator.NextDouble();
//
Wait for baseNumber to be calculated.
manualEvent.WaitOne();
//
Calculate the first term from preCalc and baseNumber.
firstTerm
=
preCalc
*
baseNumber
*
randomGenerator.NextDouble();
//
Signal that the calculation is finished.
autoEvents[
0
].Set();
}
void
CalculateSecondTerm(
object
stateInfo)
{
double
preCalc
=
randomGenerator.NextDouble();
manualEvent.WaitOne();
secondTerm
=
preCalc
*
baseNumber
*
randomGenerator.NextDouble();
autoEvents[
1
].Set();
}
void
CalculateThirdTerm(
object
stateInfo)
{
double
preCalc
=
randomGenerator.NextDouble();
manualEvent.WaitOne();
thirdTerm
=
preCalc
*
baseNumber
*
randomGenerator.NextDouble();
autoEvents[
2
].Set();
}
public
double
Result(
int
seed)
{
randomGenerator
=
new
Random(seed);
//
Simultaneously calculate the terms.
ThreadPool.QueueUserWorkItem(
new
WaitCallback(CalculateBase));
ThreadPool.QueueUserWorkItem(
new
WaitCallback(CalculateFirstTerm));
ThreadPool.QueueUserWorkItem(
new
WaitCallback(CalculateSecondTerm));
ThreadPool.QueueUserWorkItem(
new
WaitCallback(CalculateThirdTerm));
//
Wait for all of the terms to be calculated.
WaitHandle.WaitAll(autoEvents);
//
Reset the wait handle for the next calculation.
manualEvent.Reset();
return
firstTerm
+
secondTerm
+
thirdTerm;
}
}
该示例一共有4个ResetEvent,一个ManualEvent,三个AutoResetEvent,分别反映4个线程的运行状态。
ManualEvent和AutoResetEvent有一点不同:AutoResetEvent是在当前线程调用set方法激活某线程之
后,AutoResetEvent状态自动重置,而ManualEvent则需要手动调用Reset方法来重置状态。接着来看看上面那段代码的执行顺
序,Main方法首先调用的是Result
方法,Result方法开启4个线程分别去执行,主线程阻塞在WaitHandle.WaitAll(autoEvents)处,等待3个计算步骤的完
成。4个ResetEvent初始化状态都是非终止(构造实例时传入了false),CalculateBase首先执行完毕,其他3个线程阻塞在
manualEvent.WaitOne()处,等待CalculateBase执行完成。CalculateBase生成baseNumber后,把代
表自己的ManualEvent状态设置为终止状态。其他几个线程从manualEvent.WaitOne()处恢复执行,在执行完自己的代码后把自己
对应的AutoResetEvent状态置为终止。当3个计算步骤执行完后,主线程从阻塞中恢复,把三个计算结果累加后返回。还要多补充一点的是
WaitHandle的WaitOne,WaitAll,WaitAny方法,如果等待多个进程就用WaitAll,如本例中
的:WaitHandle.WaitAll(autoEvents),WaitAny是等待的线程中有一个结束则停止等待。
Mutex
对象
Mutex与Monitor类似,这里不再累赘,需要注意的是Mutex分两种:一种是本地Mutex一种是系统级Mutex,系统级Mutex可以用来
进行跨进程间的线程的同步。尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET
Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex
比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
注:文中代码示例来源于
MSDN
和
CodeProject
来自:http://www.cnblogs.com/xd125/archive/2007/12/12/992406.html