多线程:C#线程同步lock-Monitor-Mutex-同步事件和等待句柄(上)
本篇从Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的类关系图开始,希望通过本篇的介绍能对常见的线程同步方法有一个整体的认识,而对每种方式的使用细节,适用场合不会过多解释。让我们来看看这几个类的关系图:
1.lock关键字
lock是C#关键词,它将语句块标记为临界区,确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
MSDN上给出了使用lock时的注意事项通常,应避免锁定 public
类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock
(“myLock”) 违反此准则。
1)如果实例可以被公共访问,将出现
lock (this) 问题。
2)如果 MyType
可以被公共访问,将出现 lock (typeof (MyType))
问题由于一个类的所有实例都只有一个类型对象(该对象是typeof的返回结果),锁定它,就锁定了该对象的所有实例。微软现在建议不要使用lock(typeof(MyType)),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。
3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现
lock(“myLock”) 问题。这个问题和.NET
Framework创建字符串的机制有关系,如果两个string变量值都是”myLock”,在内存中会指向同一字符串对象。
最佳做法是定义
private 对象来锁定, 或 private static对象变量来保护所有实例所共有的数据。
我们再来通过IL
Dasm看看lock关键字的本质,下面是一段简单的测试代码:
{
int i = 5;
}
用IL
Dasm打开编译后的文件,上面的语句块生成的IL代码为:
IL_004a: nop
.try
{
IL_004b: nop
IL_004c: ldc.i4.5
IL_004d: stloc.1
IL_004e: nop
IL_004f: leave.s IL_0059
} // end .try
finally
{
IL_0051: ldloc.3
IL_0052: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0057: nop
IL_0058: endfinally
} // end handler
通过上面的代码我们很清楚的看到:lock关键字其实就是对Monitor类的Enter()和Exit()方法的封装,并通过try…catch…finally语句块确保在lock语句块结束后执行Monitor.Exit()方法,释放互斥锁。
2.Monitor类
Monitor类通过向单个线程授予对象锁来控制对对象的访问。对象锁提供限制访问临界区的能力。当一个线程拥有对象的锁时,其他任何线程都不能获取该锁。还可以使用
Monitor 来确保不会允许其他任何线程访问正在由锁的所有者执行的应用程序代码节,除非另一个线程正在使用其他的锁定对象执行该代码。
通过对lock关键字的分析我们知道,lock就是对Monitor的Enter和Exit的一个封装,而且使用起来更简洁,因此Monitor类的Enter()和Exit()方法的组合使用可以用lock关键字替代。
另外Monitor类还有几个常用的方法:
TryEnter()能够有效的解决长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间bool
gotLock =
Monitor.TryEnter(myobject,1000),让当前线程在等待1000秒后根据返回的bool值来决定是否继续下面的操作。
Wait()释放对象上的锁以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。脉冲信号用于通知等待线程有关对象状态的更改。
Pulse(),PulseAll()向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。
注意:Pulse、PulseAll和Wait方法必须从同步的代码块内调用。
我们假定一种情景:妈妈做蛋糕,小孩有点馋,妈妈每做好一块就要吃掉,妈妈做好一块后,告诉小孩蛋糕已经做好了。下面的例子用Monitor类的Wait和Pulse方法模拟小孩吃蛋糕的情景。
//仅仅是说明Wait和Pulse/PulseAll的例子
//逻辑上并不严密,使用场景也并不一定合适
class MonitorSample
{
private int n = 1; //生产者和消费者共同处理的数据
private int max = 10000;
private object monitor = new object();
public void Produce()
{
lock (monitor)
{
for (; n <= max; n++)
{
Console.WriteLine(“妈妈:第“ + n.ToString() + “块蛋糕做好了“);
//Pulse方法不用调用是因为另一个线程中用的是Wait(object,int)方法
//该方法使被阻止线程进入了同步对象的就绪队列
//是否需要脉冲激活是Wait方法一个参数和两个参数的重要区别
//Monitor.Pulse(monitor);
//调用Wait方法释放对象上的锁并阻止该线程(线程状态为WaitSleepJoin)
//该线程进入到同步对象的等待队列,直到其它线程调用Pulse使该线程进入到就绪队列中
//线程进入到就绪队列中才有条件争夺同步对象的所有权
//如果没有其它线程调用Pulse/PulseAll方法,该线程不可能被执行
Monitor.Wait(monitor);
}
}
}
public void Consume()
{
lock (monitor)
{
while (true)
{
//通知等待队列中的线程锁定对象状态的更改,但不会释放锁
//接收到Pulse脉冲后,线程从同步对象的等待队列移动到就绪队列中
//注意:最终能获得锁的线程并不一定是得到Pulse脉冲的线程
Monitor.Pulse(monitor);
//释放对象上的锁并阻止当前线程,直到它重新获取该锁
//如果指定的超时间隔已过,则线程进入就绪队列
Monitor.Wait(monitor,1000);
Console.WriteLine(“孩子:开始吃第“ + n.ToString() + “块蛋糕“);
}
}
}
static void Main(string[] args)
{
MonitorSample obj = new MonitorSample();
Thread tProduce = new Thread(new ThreadStart(obj.Produce));
Thread tConsume = new Thread(new ThreadStart(obj.Consume));
//Start threads.
tProduce.Start();
tConsume.Start();
Console.ReadLine();
}
}
这个例子的目的是要理解Wait和Pulse如何保证线程同步的,同时要注意Wait(obeject)和Wait(object,int)方法的区别,理解它们的区别很关键的一点是要理解同步的对象包含若干引用,其中包括对当前拥有锁的线程的引用、对就绪队列(包含准备获取锁的线程)的引用和对等待队列(包含等待对象状态更改通知的线程)的引用。