文章原始出处 http://xxinside.blogbus.com/logs/46740731.htmlhtml
预备知识:C#线程同步(1)- 临界区&Lock程序员
监视器(Monitor)的概念web
能够在MSDN(http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx)上找到下面一段话:数据库
与lock关键字相似,监视器防止多个线程同时执行代码块。Enter方法容许一个且仅一个线程继续执行后面的语句;其余全部线程都将被阻止,直到执行语句的线程调用Exit。这与使用lock关键字同样。事实上,lock 关键字就是用Monitor 类来实现的。例如:服务器
lock(x) { DoSomething(); }网络
这等效于:ide
System.Object obj = (System.Object)x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); }spa
使用 lock 关键字一般比直接使用 Monitor 类更可取,一方面是由于 lock 更简洁,另外一方面是由于 lock 确保了即便受保护的代码引起异常,也能够释放基础监视器。这是经过 finally 关键字来实现的,不管是否引起异常它都执行关联的代码块。.net
这里微软已经说得很清楚了,Lock就是用Monitor实现的,二者都是C#中对临界区功能的实现。用ILDASM打开含有如下代码的exe或者dll也能够证明这一点(我并无本身证明):线程
lock (lockobject) { int i = 5; }
反编译后的的IL代码为:
IL_0045: call
void [mscorlib]System.Threading.Monitor::Enter(object)
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
Monitor中和lock等效的方法
Monitor是一个静态类,所以不能被实例化,只能直接调用Monitor上的各类方法来完成与lock相同的功能:
上篇中提到的有关lock的全部使用方法和建议,都适用于它们。
比lock更“高级”的Monitor
到此为止,全部见到的仍是咱们在lock中熟悉的东西,再看Monitor的其它方法以前,咱们来看看那老掉牙的“生产者和消费者”场景。试想消费者和生产者是两个独立的线程,同时访问一个容器:
粗看这个场景并无什么特殊的问题,只要在两个线程中分别调用两个方法,这两个方法内部都用同一把锁进入临界区访问容器便可。但是问题在于:
二者选择直接退出不会引起什么问题,无非就是可能屡次无功而返。这么作,你的程序逻辑老是有机会获得正确执行的,可是效率很低,由于这样的机制自己是不可控的,业务逻辑是否得以成功执行彻底是随机的。
因此咱们须要更有效、更“优雅”的方式:
在按这个思路写出Sample Code前,咱们来看Monitor上须要用的其它重要方法:
好了,有了它们咱们就能够完成这样的代码:
using System; using System.Threading; using System.Collections; using System.Linq; using System.Text;
class MonitorSample
{
//容器,一个只能容纳一块糖的糖盒子。PS:如今MS已经不推荐使用ArrayList,
//支持泛型的List才是应该在程序中使用的,我这里偷懒,不想再去写一个Candy类了。
private ArrayList _candyBox = new ArrayList(1);
private volatile bool _shouldStop = false; //用于控制线程正常结束的标志
/// <summary>
/// 用于结束Produce()和Consume()在辅助线程中的执行
/// </summary>
public void StopThread()
{
_shouldStop = true;
//这时候生产者/消费者之一可能由于在阻塞中而没有机会看到结束标志,
//而另外一个线程顺利结束,因此剩下的那个必定长眠不醒,须要咱们在这里尝试叫醒它们。
//不过这并不能确保线程能顺利结束,由于可能咱们刚刚发送信号之后,线程才阻塞本身。
Monitor.Enter(_candyBox);
try
{
Monitor.PulseAll(_candyBox);
}
finally
{
Monitor.Exit(_candyBox);
}
}
/// <summary>
/// 生产者的方法
/// </summary>
public void Produce()
{
while(!_shouldStop)
{
Monitor.Enter(_candyBox);
try
{
if (_candyBox.Count==0)
{
_candyBox.Add("A candy");
Console.WriteLine("生产者:有糖吃啦!");
//唤醒可能如今正在阻塞中的消费者
Monitor.Pulse(_candyBox);
Console.WriteLine("生产者:赶快来吃!!");
//调用Wait方法释放对象上的锁,并使生产者线程状态转为WaitSleepJoin,阻止该线程被CPU调用(跟Sleep同样)
//直到消费者线程调用Pulse(_candyBox)使该线程进入到Running状态
Monitor.Wait(_candyBox);
}
else //容器是满的
{
Console.WriteLine("生产者:糖罐是满的!");
//唤醒可能如今正在阻塞中的消费者
Monitor.Pulse(_candyBox);
//调用Wait方法释放对象上的锁,并使生产者线程状态转为WaitSleepJoin,阻止该线程被CPU调用(跟Sleep同样)
//直到消费者线程调用Pulse(_candyBox)使生产者线程从新进入到Running状态,此才语句返回
Monitor.Wait(_candyBox);
}
}
finally
{
Monitor.Exit(_candyBox);
}
Thread.Sleep(2000);
}
Console.WriteLine("生产者:下班啦!");
}
/// <summary>
/// 消费者的方法
/// </summary>
public void Consume()
{
//即使看到结束标致也应该把容器中的全部资源处理完毕再退出,不然容器中的资源可能就此丢失
//不过这里_candyBox.Count是有可能读到脏数据的,好在咱们这个例子中只有两个线程因此问题并不突出
//正式环境中,应该用更好的办法解决这个问题。
while (!_shouldStop || _candyBox.Count > 0)
{
Monitor.Enter(_candyBox);
try
{
if (_candyBox.Count==1)
{
_candyBox.RemoveAt(0);
if (!_shouldStop)
{
Console.WriteLine("消费者:糖已吃完!");
}
else
{
Console.WriteLine("消费者:还有糖没吃,立刻就完!");
}
//唤醒可能如今正在阻塞中的生产者
Monitor.Pulse(_candyBox);
Console.WriteLine("消费者:赶快生产!!");
Monitor.Wait(_candyBox);
}
else
{
Console.WriteLine("消费者:糖罐是空的!");
//唤醒可能如今正在阻塞中的生产者
Monitor.Pulse(_candyBox);
Monitor.Wait(_candyBox);
}
}
finally
{
Monitor.Exit(_candyBox);
}
Thread.Sleep(2000);
}
Console.WriteLine("消费者:都吃光啦,下次再吃!");
}
static void Main(string[] args)
{
MonitorSample ss = new MonitorSample();
Thread thdProduce = new Thread(new ThreadStart(ss.Produce));
Thread thdConsume = new Thread(new ThreadStart(ss.Consume));
//Start threads.
Console.WriteLine("开始启动线程,输入回车终止生产者和消费者的工做……\r\n******************************************");
thdProduce.Start();
Thread.Sleep(2000); //尽可能确保生产者先执行
thdConsume.Start();
Console.ReadLine();
//经过IO阻塞主线程,等待辅助线程演示直到收到一个回车
ss.StopThread(); //正常且优雅的结束生产者和消费者线程
Thread.Sleep(1000); //等待线程结束
while (thdProduce.ThreadState != ThreadState.Stopped)
{
ss.StopThread(); //线程尚未结束有多是由于它自己是阻塞的,尝试使用StopThread()方法中的PulseAll()唤醒它,让他看到结束标志
thdProduce.Join(1000); //等待生产这线程结束
}
while (thdConsume.ThreadState != ThreadState.Stopped)
{
ss.StopThread();
thdConsume.Join(1000); //等待消费者线程结束
}
Console.WriteLine("******************************************\r\n输入回车结束!");
Console.ReadLine();
}
}
可能的几种输出(不是所有可能):
开始启动线程,输入回车终止生产者和消费者的工做…… ****************************************** 生产者:有糖吃啦! 生产者:赶快来吃!!
消费者:还有糖没吃,立刻就完! 消费者:赶快生产!! 生产者:下班啦! 消费者:都吃光啦,下次再吃! ****************************************** 输入回车结束!
开始启动线程,输入回车终止生产者和消费者的工做…… ****************************************** 生产者:有糖吃啦! 生产者:赶快来吃!! 消费者:糖已吃完! 消费者:赶快生产!!
生产者:下班啦! 消费者:都吃光啦,下次再吃! ****************************************** 输入回车结束!
开始启动线程,输入回车终止生产者和消费者的工做…… ****************************************** 生产者:有糖吃啦! 生产者:赶快来吃!! 消费者:糖已吃完! 消费者:赶快生产!! 生产者:有糖吃啦! 生产者:赶快来吃!!
消费者:还有糖没吃,立刻就完! 消费者:赶快生产!! 生产者:下班啦! 消费者:都吃光啦,下次再吃! ****************************************** 输入回车结束!
有兴趣的话你还能够尝试修改生产者和消费者的启动顺序,尝试下其它的结果(好比糖罐为空)。其实生产者和消费者方法中那个Sleep(2000)也是为了方便手工尝试出不一样分支的执行状况,输出中的空行就是我敲入回车让线程停止的时机。
你可能已经发现,除非消费者先于生产者启动,不然咱们永远不会看到消费者说“糖罐是空的!”,这是由于消费者在吃糖之后把本身阻塞了,直到生产者生产出糖块后唤醒本身。另外一方面,生产者即使先于消费者启动,在这个例子中咱们也永远不会看到生产者说“糖罐是满的!”,由于初始糖罐为空且生产者在生产后就把本身阻塞了。
题外话1: 是否是以为生产者判断糖罐是满的、消费者检查出糖罐是空的分支有些多余? 想一想,若是糖罐初始也许并不为空,又或者消费者先于生产者执行,那么它们就会派上用场。这毕竟只是一个例子,咱们在没有任何限制条件下设计了这个环环相扣的简单场景,因此让这两个分支“显得”有些多余,但大多数真实状况并不如此。 在实际应用中,生产者每每表明负责从某处简单接收资源的线程,好比来自网络的指令、从服务器返回的查询等等;而消费者线程须要负责解析指令、解析返回的查询结果,而后存储到本地数据库、文件或者呈现给用户等等。消费者线程的任务每每更复杂,执行时间更长,为了提升程序的总体执行效率,消费者线程每每会多于生产者线程,可能3对1,也可能5对2…… CPU的随机调度,可能会形成各类各样的状况。你基本上是没法预测一段代码在被调用时,与之相关的外部环境是怎样的,因此完备的处理每个分支是必要的。 另外一方面,即使一个分支的状况不是咱们设计中指望发生的,可是因为某种如今没法预见的错误,形成本“不可能”、“不该该”出现的分支得以执行,那么在这个分支的代码能够保障你的业务逻辑能够在错误的异常状况下得以修正,至少你也能够报警避免更大的错误。 因此老是建议给每一个if都写上else分支,这除了让你的代码显得更加仅仅有条、逻辑清晰外,还可能给你带来额外的扩展性和健壮性。就像在前一篇中所提到的,不要由于别人(你所写类的使用者)的“错误”(谁让你给别人这个机会呢?)连累本身!
题外话2:
你能够用微软的建议用
lock(_candyBox){...}
替代上面代码中的
Monitor.Enter(_candyBox);
try{...}
finally
{
Monitor.Exit(_candyBox);
},这里我不作任何反对。不过在更多时候,你核能会须要在finally里作更多的事情,而不仅是Exit那么简单,因此即使用了lock,你还得本身写try/finally。
若是你的头已经有些晕了,那么立刻跳过这个题外话,下面说的跟线程同步毫无关系。这个题外话其实想引伸到using。这个C#特有的(其它.net语言没有相似语法)关键字,它会帮你自动调用全部实现了IDisposable接口类上的Dispose()方法。跟lock相似,using(obj) {//do something}等效于一个以下的try/finally语句块:
SS obj = new SS();
try
{
//use obj to do something
}
finally
{
obj.Dispose();
}
微软一厢情愿的但愿经过using避免程序员忘记调用Dispose()去释放该类所占用的那些资源,包括托管的和非托管的(磁盘IO、网络IO、数据库链接IO等等),你一般会在关于磁盘操做的类、各类Stream、网络操做相关的类、数据库驱动类上找到这个方法。Dispose()里主要是替你Disconnet()/Close()掉这些资源,可是这些Dispose()方法经常是由微软以外的公司编写的,好比Oracle的.Net驱动。你能确信Oracle的程序员很是了解Dispose()在.net中的重要含义么?回头来讲,就算是微软本身的程序员,难道就不会犯错误吗?跟lock中提到的SynRoot实现同样,你根本不知道你所使用类的Dispose()是不是正确的,也没法确保下一个版本的Dispose()不会悄悄的改变……对于这些敏感的资源,本身老老实实去Disconnect()/Close(),再老老实实的去Dispose()。事实上finally须要作的事情也每每不仅是一个Dispose()。 一句话,关于using,坚定反对。
就到这里吧,好累~