C#线程同步(2)- 临界区&Monitor

文章原始出处 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相同的功能:

  • Enter(object)/TryEnter(object)/TryEnter(object, int32)/TryEnter(object, timespan):用来获取对象锁(Lock中已经提到过,这里再强调一次,是对象类型而不能是值类型),标记临界区的开始。与Enter不一样,TryEnter永远不会阻塞代码,当没法获取对象锁时它会返回False,而且调用者不进入临界区。TryEnter还有两种重载,能够定义一个时间段,在该时间段内一直尝试得到对象锁,超时则返回False。
  • Exit(object):没啥好说的,释放对象锁、退出临界区。只是必定记得在try的finally块里调用,不然一但因为异常形成Exit没法执行,对象锁得不到释放,就会形成死锁。此外,调用Exit的线程必须拥有 object 参数上的锁,不然会引起SynchronizationLockException异常。在调用线程获取指定对象上的锁后,能够重复对该对象进行了相同次数的 Exit 和 Enter 调用;若是调用 Exit 与调用 Enter 的次数不匹配,那么该锁不会被正确释放。

  上篇中提到的有关lock的全部使用方法和建议,都适用于它们。

比lock更“高级”的Monitor

  到此为止,全部见到的仍是咱们在lock中熟悉的东西,再看Monitor的其它方法以前,咱们来看看那老掉牙的“生产者和消费者”场景。试想消费者和生产者是两个独立的线程,同时访问一个容器:

  • 很显然这个容器是一个临界资源(你不会问我为何是显然吧?),同时只容许一个线程访问。
  • 生产者往容器里存放生产好的资源;消费者消费掉容器里的资源。

  粗看这个场景并无什么特殊的问题,只要在两个线程中分别调用两个方法,这两个方法内部都用同一把锁进入临界区访问容器便可。但是问题在于:

  • 消费者锁定容器,进入临界区后可能发现容器是空的。它能够退出临界区,而后下次再盲目地进入碰碰运气;若是不退出,那么让生产者永远没法进入临界区,往容器里放入资源供消费者消费,从而形成死锁。
  • 而生产者也可能进入临界区后,却发现容器是满的。结果同样,直接退出等下次来碰运气;或者不退出形成死锁。

  二者选择直接退出不会引起什么问题,无非就是可能屡次无功而返。这么作,你的程序逻辑老是有机会获得正确执行的,可是效率很低,由于这样的机制自己是不可控的,业务逻辑是否得以成功执行彻底是随机的。

  因此咱们须要更有效、更“优雅”的方式:

  • 消费者在进入临界区发现容器为空后,当即释放锁并把本身阻塞,等待生产者通知,再也不作无谓的尝试;若是顺利消费资源完毕后,主动通知生产者能够进行生产了,随后仍然阻塞本身等待生产者通知。
  • 生产者若是发现容器是满的,那么当即释放锁并阻塞本身,等待消费者在消费完成后唤醒;在生产完毕后,主动给消费者发出通知,随后也仍然阻塞本身,等待消费者告诉本身容器已经空了。

  在按这个思路写出Sample Code前,咱们来看Monitor上须要用的其它重要方法:

  • Wait(Object)/Wait(Object, Int32)/Wait(Object, TimeSpan)/Wait(Object, Int32, Boolean)/Wait(Object, TimeSpan, Boolean):  释放对象上的锁并阻塞当前线程,直到它从新获取该锁。
    1. 这里的阻塞是指当前线程进入“WaitSleepJoin”状态,此时CPU再也不会分配给这种状态的线程CPU时间片,这其实跟在线程上调用Sleep()时的状态同样。这时,线程不会参与对该锁的分配争夺。
    2. 要打破这种状态,须要其它拥有该对象锁的线程,调用下面要讲到的Pulse()来唤醒。不过这与,Sleep()不一样,只有那些由于该对象锁阻塞的线程才会被唤醒。此时,线程从新进入“Running”状态,参与对对象锁的争夺。
    3. 强调一下,Wait()其实起到了Exit()的做用,也就是释放当前所得到的对象锁。只不过Wait()同时又阻塞了本身。
    4. 咱们还看到Wait()的几个重载方法。其中第二、3个方法给Wait加上了一个时间,若是超时Wait会返回再也不阻塞,而且能够根据Wait 方法的返回值,以肯定它是否已在超时前从新获取锁。在这种状况下,其实线程并不须要等待其它线程Pulse()唤醒,至关于Sleep必定时间后醒来。第四、5个方法在第二、3个方法的基础上加上exitContent参数,咱们暂时不去管它,你能够详细参见这里:http://msdn.microsoft.com/zh-cn/library/79fkfcw1(VS.85).aspx
  • Pulse(object):向阻塞线程队列(因为该object而转入WaitSleepJoin状态的全部线程,也就是那些执行了Wait(object)的线程,存放的队列)中第一个线程发信号,该信号通知锁定对象的状态已更改,而且锁的全部者准备释放该锁。收到信号的阻塞线程进入就绪队列中(那些处于Running状态的线程,能够被CPU调用运行的线程在这个队列里),以便它有机会接收对象锁。注意,接受到信号的线程只会从阻塞中被唤醒,并不必定会得到对象锁。
  • PulseAll(object):与Pulse()不一样,阻塞队列中的全部线程都会收到信号,并被唤醒转入Running状态,即进入就绪队列中。至于它们谁会幸运的得到对象锁,那就要看CPU了。
  • 注意:以上全部方法都只能在临界区内被调用,换句话说,只有对象锁的得到者可以正确调用它们,不然会引起SynchronizationLockException异常。 

  好了,有了它们咱们就能够完成这样的代码:

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,坚定反对。

  就到这里吧,好累~

相关文章
相关标签/搜索