白话并发冲突与线程同步(2)——Monitor、lock和死锁

1-2-3 和比尔盖茨的一些往事 在上一篇里我们说道,1-2-3写了一段程序,并且在使用了2个线程分别执行foo1()和foo2()之后,程序的结果就不对了。
class  Program {      static   int  n  =   0 ;      static   void  foo1()     {          for  ( int  i  =   0 ; i  <   1000000000 ; i ++ //  10 浜?/span>         {                 int a = n;                 n = a + 1;         }         Console.WriteLine("foo1() complete n = {0}", n);     }     static void foo2()     {         for (int j = 0; j < 1000000000; j++// 10 浜?/span>         {                 int a = n;                 n = a + 1;         }         Console.WriteLine("foo2() complete n = {0}", n);     }     static void Main(string[] args)     {         new Thread(foo1).Start();         new Thread(foo2).Start();     } }
究其原因,就是因为Windows总是不问青红皂白随随便便就把我的线程给停掉了。例如,上面的那个程序很可能会以下面的顺序来执行(黄色底色的代码属于第一个线程,绿色底色的代码属于第二个线程): 这样,第一、第二个线程里面的循环各自执行了3次,n的值是3,而不是我们期望的6。 所以呢,我就打算建议比尔盖茨在C#里加一个关键字: 对foo2()也做同样的修改,这样,就可以确保程序以下图所示的顺序执行了: 如果这个建议被微软接受,它将创造两个记录:   1. 它将是C#里面第一个中文关键字。   2. 它将是C#里面最长的关键字。 可是,比尔盖茨听了我的建议之后,却把眉毛皱成了个大疙瘩,叹道:“大哥,不行呀。你知道,Windows里会同时运行着上千个线程,且不说那些居心不良的病毒和木马,就是那些干正经事的线程,谁又能保证在你那个超长关键字里包裹的代码不会运行个二、三十秒?CPU可只有一个,在那个线程运行的二、三十秒里,整个Windows都会一动不动的,不知情的用户还以为是Windows又挂掉了,最后挨骂的可是兄弟我呦!” “不过,”比尔又接着说,“我可以提供另一种方案来达到同样的效果。我可以让线程1里面的指定代码块不执行完,线程2就一直处于阻塞(ThreadState.WaitSleepJoin)状态。” 要达到这个效果,需要使用.net里的两个函数。 Monitor.Enter(n); // 尝试获取对n的控制权。如果n没主儿,则成功获取了n的控制权;如果n已经有主儿了,则此线程阻塞,死等。 Monitor.Exit(n); // 释放对n的控制权。等待着n的那个阻塞中的线程将获取n的控制权,并从阻塞状态变成运行状态。 可以把n想像成WC里的一个蹲位,线程1 Enter了之后,其它线程就不能Enter了,只能干等着,直到线程1 Exit,下一个等着的线程才能Enter,之后才能继续办事。如果一个线程Enter了之后迟迟不Exit(例如Enter了之后,发生了异常,比如忘了带SZ),就是所谓的“占着MK不LS”了。(一边吃午饭一边看贴的兄弟对不住啦~~) 使用 Monitor 现在就可以在我的代码里使用Monitor了。
class  Program {      static   int  n  =   0 ;      static   void  foo1()     {          for  ( int  i  =   0 ; i  <   1000000000 ; i ++ //  10 亿         {             Monitor.Enter(n);              int  a  =  n;             n  =  a  +   1 ;             Monitor.Exit(n);         }         Console.WriteLine( " foo1() complete n = {0} " , n);     }      static   void  foo2()     {          for  ( int  j  =   0 ; j  <   1000000000 ; j ++ //  10 亿         {             Monitor.Enter(n);              int  a  =  n;             n  =  a  +   1 ;             Monitor.Exit(n);         }         Console.WriteLine( " foo2() complete n = {0} " , n);     }      static   void  Main( string [] args)     {          new  Thread(foo1).Start();          new  Thread(foo2).Start();     } }
这段代码很可能会以下图所示的顺序执行(黄色底色的代码属于线程1,绿色底色的代码属于线程2。下图演示了线程1循环2次,线程2循环1次,n的值为3): 如果我们把上图之中与Monitor相关的行和演示线程状态的行去掉,就可以得到下图: 怎么样?和我的那个超长关键字的效果一样吧? 不过,如果你尝试运行上面那个代码,就会发现它根本无法通过编译!这是因为Monitor.Enter()只接受类型为Object的参数。那么,可不可以写 Monitor.Enter( (Object)n); 呢?它确实能够通过编译,但是这样岂不是要装箱20亿次?所以千万别这么写。没法子了,我们只能再声明一个Object类型的变量,专门用于这两个线程的同步。
class  Program {      static   int  n  =   0 ;      static object mk = new object();      static   void  foo1()     {          for  ( int  i  =   0 ; i  <   1000000000 ; i ++ //  10 亿         {             Monitor.Enter(mk);              int  a  =  n;             n  =  a  +   1 ;             Monitor.Exit(mk);         }         Console.WriteLine( " foo1() complete n = {0} " , n);     }      static   void  foo2()     {          for  ( int  j  =   0 ; j  <   1000000000 ; j ++ //  10 亿         {             Monitor.Enter(mk);              int  a  =  n;             n  =  a  +   1 ;             Monitor.Exit(mk);         }         Console.WriteLine( " foo2() complete n = {0} " , n);     }      static   void  Main( string [] args)     {          new  Thread(foo1).Start();          new  Thread(foo2).Start();     } }
这段代码在我的赛扬800的机器上运行时间为3分零6秒。 lock 关键字 在C#里面有一个lock关键字,它其实是一个语法糖。 小贴士:在VB里与lock等价的关键字是SyncLock。用法是
SyncLock  (mk)      Dim  a  As   Integer   =  n     n  =  a  +   1 End   SyncLock
死锁 还有比占着MK不LS更恶劣的行径么?有,那就是吃着碗里的望着锅里的。在下面的这段代码中,线程1喜欢先占着mk1然后在mk2里办事;线程2呢,喜欢先占着mk2,然后在mk1里办事,要是这两个活宝碰到一起……
class  Program {      static   object  mk1  =   new   object ();      static   object  mk2  =   new   object ();      static   void  foo1()     {          for  ( int  i  =   0 ; i  <   100 ; i ++ )         {             Monitor.Enter(mk1);             Console.WriteLine( " i={0} 线程1:/ " 先占着mk1,再去mk2里办事。/ "" , i);             Monitor.Enter(mk2);             Console.WriteLine( " i={0} 线程1:/ " 进入了mk2,办事/ "" , i);             Monitor.Exit(mk2);             Console.WriteLine( " i={0} 线程1:/ " 办完事了,离开mk2/ "" , i);             Monitor.Exit(mk1);             Console.WriteLine( " i={0} 线程1:/ " 办完事了,离开mk1/ "" , i);         }     }      static   void  foo2()     {          for  ( int  j  =   0 ; j  <   100 ; j ++ )         {             Monitor.Enter(mk2);             Console.WriteLine( " j={0} 线程2:/ " 先占着mk2,再去mk1里办事。/ "" , j);             Monitor.Enter(mk1);             Console.WriteLine( " j={0} 线程2:/ " 进入了mk1,办事/ "" , j);             Monitor.Exit(mk1);             Console.WriteLine( " j={0} 线程2:/ " 办完事了,离开mk1/ "" , j);             Monitor.Exit(mk2);             Console.WriteLine( " j={0} 线程2:/ " 办完事了,离开mk2/ "" , j);         }     }      static   void  Main( string [] args)     {          new  Thread(foo1).Start();          new  Thread(foo2).Start();     } }
运行这段代码,可以得到这样的结果: 如上图所示,当程序恰巧以“线程1 Enter mk1 -> 线程2 Enter mk2 -> 线程1 想要Enter mk2 发现 mk2 已经被占用,线程1阻塞 -> 线程2 想要Enter mk1 发现 mk1 己经被占用,线程2阻塞”这个顺序执行时,线程1等待线程2释放mk2,线程2等待线程1释放mk1,两个线程双双陷入阻塞状态,直到山无棱、天地合……这就是死锁。 参考文献 Jeffrey Richter, CLR via C#, Second Edition. Microsoft Press, 2006.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值