我们知道lock实际上一个语法糖糖,C#编译器实际上把他展开为Monitor.Enter和Monitor.Exit,即:

  1. lock(lockObj)
  2. {
  3. //...
  4. }
  5.  
  6. ////相当于(.Net4以前):
  7.  
  8. Monitor.Enter(lockObj);
  9.  
  10. try
  11. {
  12. //...
  13. }
  14.  
  15. finally
  16. {
  17. Monitor.Exit(lockObj);
  18. }

但是,这种实现逻辑至少理论上有一个错误:当Monitor.Enter(lockObj);刚刚完成,还没有进入try区的时候,有可能从其他线程发出了Thread.Abort等命令,使得该线程没有机会进入try...finally。也就是说lockObj没有办法得到释放,有可能造成程序死锁。这也是Thread.Abort一般被认为是邪恶的原因之一。

DotNet4开始,增加了Monitor.Enter(object,ref bool)重载。而C#编译器会把lock展开为更安全的Monitor.Enter(object,ref bool)和Monitor.Exit:

  1. lock(lockObj)
  2. {
  3. //...
  4. }
  5.  
  6. ////相当于(DotNet 4):
  7.  
  8. bool lockTaken = false;
  9.  
  10. try
  11. {
  12. Monitor.Enter(lockObj,ref lockTaken);
  13. //
  14. }
  15.  
  16. finally
  17. {
  18. if (lockTaken) Monitor.Exit(lockObj);
  19. }
  1.  

现在Monitor.TryEnter在try的保护下,“加锁”成功意味着“放锁”将得到finally的保护。

有一个问题,“Lock关键字不是有获取锁、释放锁的功能吗?...为什么还需要执行Pulse?”
也有朋友有些疑点,“用lock就不要用monitor了”,“Monitor.Wait完全没必要”,“为什么Pulse和Wait方法必须从同步的代码块内调用?”

这些疑问很自然。在大部分情况下,lock确实能基本达到我们要求资源同步的目的,加上配合其他同步工具,比如事件(AutoResetEvent)等的应用,日常工作中确实没有太多机会需要用到Monitor.Wait和Pulse。不过,虽然较少机会用到,事实上Wait和Pulse跟lock完全不是一回事。他们提供了更细腻的同步功能,能达到lock作不来的功能。

为更好的回答和解释这些疑问,该帖将首先介绍Wait和Pulse的用途,通过一个简单例子逐条分析同步的过程;然后提供一个用轻量级的lock,Wait和Pulse来实现一个事件通知的实例;最后谈谈DotNet4对lock编译展开的一点有趣变化。

让我们首先看看MSDN对Monitor.Wait的解释(链接见注释):
释放对象上的锁并阻止当前线程,直到它重新获取该锁。...

该解释的确很粗糙,很难理解。让我们来看看它下面的备注:
同步的对象包含若干引用,其中包括对当前拥有锁的线程的引用、对就绪队列的引用和对等待队列的引用。

这个多少还给了点东西,现在我们脑海中想像这么一幅图画:

  1.          |- 拥有锁的线程
  2. lockObj->|- 就绪队列(ready queue)
  3.          |- 等待队列(wait queue)

当一个线程尝试着lock一个同步对象的时候,该线程就在就绪队列中排队。一旦没人拥有该同步对象,就绪队列中的线程就可以占有该同步对象。这也是我们平时最经常用的lock方法。
为了其他的同步目的,占有同步对象的线程也可以暂时放弃同步对象,并把自己流放到等待队列中去。这就是Monitor.Wait。由于该线程放弃了同步对象,其他在就绪队列的排队者就可以进而拥有同步对象。
比起就绪队列来说,在等待队列中排队的线程更像是二等公民:他们不能自动得到同步对象,甚至不能自动升舱到就绪队列。而Monitor.Pulse的作用就是开一次门,使得一个正在等待队列中的线程升舱到就绪队列;相应的Monitor.PulseAll则打开门放所有等待队列中的线程到就绪队列。

比如下面的程序:

  1. static void Main(string[] args)
  2. {
  3.  
  4. //设置Priority,确保按代码顺序执行
  5.  
  6. Thread a = new Thread(A);
  7.  
  8. a.Priority = ThreadPriority.Highest;
  9.  
  10. a.Start();
  11.  
  12. Thread b = new Thread(B);
  13.  
  14. b.Priority = ThreadPriority.Normal;
  15.  
  16. b.Start();
  17.  
  18. Thread c = new Thread(C);
  19.  
  20. c.Priority = ThreadPriority.Lowest;
  21.  
  22. c.Start();
  23.  
  24. Console.ReadLine();
  25.  
  26. }
  1. static object lockObj = new object();
  2.  
  3. static void A()
  4. {
  5. lock (lockObj) //进入就绪队列
  6. {
  7. //因为A所在的线程Priority高,所以会先进来
  8. Console.WriteLine("Into A lock block");
  9. Thread.Sleep();
  10.  
  11. //这里调用了Monitor.Pulse(lockObj),会通知lockObj的等待队列
  12. //但现在lockObj的等待队列没有线程在等待,所以通知不被处理
  13. Monitor.Pulse(lockObj);
  14.  
  15. //自我流放到等待队列
  16. Monitor.Wait(lockObj);//同时此线程在此停止
  17. //线程此时的状态是WaitSleepJoin
  18. //虽然在Monitor.Wait之前有Monitor.Pulse通知等待队列,但是过期无效。
  19. //无论之前有多少个Monitor.Pulse
  20. //本线程在等待队列后接受到Monitor.Pulse的通知时,线程回到就绪队列
  21. //从就绪队列中出来的时候,线程在此恢复
  22. }
  23. Console.WriteLine("A exit...");
  24. }
  25.  
  26. static void B()
  27. {
  28. lock (lockObj) //进入就绪队列
  29. //B和C都在就绪队列,因为B所在的线程Priority比C高,所以会先进来
  30. Console.WriteLine("Into B lock block");
  31.  
  32. //这里调用了Monitor.Pulse(lockObj),会通知lockObj的等待队列
  33. //于此同时,A 已经在等待队列了
  34. Monitor.Pulse(lockObj);
  35. Console.WriteLine("B Call Pulse");
  36. }
  37. Console.WriteLine("B lock block exit...");
  38. Thread.Sleep();
  39. Console.WriteLine("B exit...");
  40. }
  41.  
  42. static void C()
  43. {
  44. //如果CPU是低负载理想的情况,此时的就绪队列中会有两个线程
  45. //除了C还有接收到信号的A,C排在前面理想情况会先进来 但不是绝对,有时会是A
  46. lock (lockObj) //进入就绪队列
  47. {
  48. Console.WriteLine("Into C lock block");
  49. }
  50. Console.WriteLine("C exit...");
  51. }

从时间线上来分析:

  1. T 线程A
  2.  
  3. lock( lockObj )
  4.  
  5. {
  6.  
  7. //... 线程B 线程C
  8.  
  9. //... lock( lockObj ) lock( lockObj )
  10.  
  11. //... { {
  12.  
  13. //... //...
  14.  
  15. //... //...
  16.  
  17. Monitor.Pulse //...
  18.  
  19. Monitor.Wait //...
  20.  
  21. //... Monitor.Pulse
  22.  
  23. //... } }
  24.  
  25. }
  1.  

时间点0,假设线程A先得到了同步对象,它就登记到同步对象lockObj的“拥有者引用”中。

时间点3,线程B和C要求拥有同步对象,他们将在“就绪队列”排队:

|--(拥有锁的线程) A

|

3 lockObj--|--(就绪队列) B,C

|

|--(等待队列)

时间点7,线程A用Pulse发出信号,允许第一个正在"等待队列"中的线程进入到”就绪队列“。但由于就绪队列是空的,什么事也没有发生。

时间点8,线程A用Wait放弃同步对象,并把自己放入"等待队列"。B,C已经在就绪队列中,因此其中的一个得以获得同步对象(假定是B)。B成了同步

对象的拥有者。C现在还是候补委员,可以自动获得空缺。而A则被关在门外,不能自动获得空缺。

|--(拥有锁的线程) B

|

8 lockObj--|--(就绪队列) C

|

|--(等待队列) A

时间点9,线程B用Pulse发出信号开门,第一个被关在门外的A被允许放入到就绪队列,现在C和A都成了候补委员,一旦同步对象空闲,都有机会得它。

|--(拥有锁的线程) B

|

9 lockObj--|--(就绪队列) C,A

|

|--(等待队列)

时间点10,线程B退出Lock区块,同步对象闲置,就绪队列队列中的C或A就可以转正为拥有者(假设C得到了同步对象)。

|--(拥有锁的线程) C

|

10 lockObj--|--(就绪队列) A

|

|--(等待队列)

随后C也退出Lock区块,同步对象闲置,A就重新得到了同步对象,并从Monitor.Wait中返回...

最终的执行结果就是:

B exit...

C exit...

A exit...

顺序不是固定的,而是要结合CPU等系统资源的调配

Pulse和PulseAll方法,这两个方法就是把锁状态将要改变的消息通知给等待队列中的线程,不过这时如果等待队列中没有线程,那么该方法就会一直等待下去,直到有等待的线程进入队列,也就是说该方法可能造成类试死锁的情况出现。

由于Monitor.Wait的暂时放弃和Monitor.Pulse的开门机制,我们可以用Monitor来实现更丰富的同步机制,比如一个事件机(ManualResetEvent):

  1. class MyManualEvent
  2. {
  3. private object lockObj = new object();
  4.  
  5. private bool hasSet = false;
  6.  
  7. public void Set()
  8. {
  9. lock (lockObj)
  10. {
  11. hasSet = true;
  12. Monitor.PulseAll(lockObj);
  13. }
  14.  
  15. }
  16.  
  17. public void WaitOne()
  18. {
  19. lock (lockObj)
  20. {
  21. while (!hasSet)
  22. {
  23. Monitor.Wait(lockObj);
  24. }
  25. }
  26.  
  27. }
  28.  
  29. }

  30. class Program
  31. {
  32. static MyManualEvent myManualEvent = new MyManualEvent();
  33.  
  34. static void Main(string[] args)
  35. {
  36. ThreadPool.QueueUserWorkItem(WorkerThread, "A");
  37.  
  38. ThreadPool.QueueUserWorkItem(WorkerThread, "B");
  39.  
  40. Console.WriteLine("Press enter to signal the green light");
  41.  
  42. Console.ReadLine();
  43.  
  44. myManualEvent.Set();
  45.  
  46. ThreadPool.QueueUserWorkItem(WorkerThread, "C");
  47.  
  48. Console.ReadLine();
  49. }
  50.  
  51. static void WorkerThread(object state)
  52. {
  53. myManualEvent.WaitOne();
  54. Console.WriteLine("Thread {0} got the green light...", state);
  55. }
  56. }
  1.  

我们看到了该玩具MyManualEvent实现了类库中的ManulaResetEvent的功能,但却更加的轻便 - 类库的ManulaResetEvent使用了操作系统内核事件机制,负担比较大(不算竞态时间,ManulaResetEvent是微秒级,而lock是几十纳秒级)。

例子的WaitOne中先在lock的保护下判断是否信号绿灯,如果不是则进入等待。因此可以有多个线程(比如例子中的AB)在等待队列中排队。
当调用Set的时候,在lock的保护下信号转绿,并使用PulseAll开门放狗,将所有排在等待队列中的线程放入就绪队列,A或B(比如A)于是可以重新获得同步对象,从Monitor.Wait退出,并随即退出lock区块,WaitOne返回。随后B或A(比如B)重复相同故事,并从WaitOne返回。
线程C在myManualEvent.Set()后才执行,它在WaitOne中确信信号灯早已转绿,于是可以立刻返回并得以执行随后的命令。

该玩具MyManualEvent可以用在需要等待初始化的场合,比如多个工作线程都必须等到初始化完成后,接到OK信号后才能开工。该玩具MyManualEvent比起ManulaResetEvent有很多局限,比如不能跨进程使用,但它演示了通过基本的Monitor命令组合,达到事件机的作用。

现在是回答朋友们的疑问的时候了:
Q: Lock关键字不是有获取锁、释放锁的功能... 为什么还需要执行Pulse?
A: 因为Wait和Pulse另有用途。

Q: 用lock 就不要用monitor了(?)
A: lock只是Monitor.Enter和Monitor.Exit,用Monitor的方法,不仅能用Wait,还可以用带超时的Monitor.Enter重载。

Q: Monitor.Wait完全没必要 (?)
A: Wait和Pulse另有用途。

Q: 什么Pulse和Wait方法必须从同步的代码块内调用?
A: 因为Wait的本意就是“[暂时]释放对象上的锁并阻止当前线程,直到它重新获取该锁”,没有获得就谈不到释放。

多线程中的lock,Monitor.Wait和Monitor.Pulse的更多相关文章

  1. 多线程中的Lock小结

    出处:http://www.cnblogs.com/DarrenChan/p/6528578.html#undefined 1.lock和synchronized的区别 1)Lock不是Java语言内 ...

  2. c#初学-多线程中lock用法的经典实例

    本文转载自:http://www.cnblogs.com/promise-7/articles/2354077.html 一.Lock定义     lock 关键字可以用来确保代码块完成运行,而不会被 ...

  3. 多线程中lock用法的经典实例

    多线程中lock用法的经典实例 一.Lock定义     lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断.它可以把一段代码定义为互斥段(critical section),互斥段在一 ...

  4. python 多线程中的同步锁 Lock Rlock Semaphore Event Conditio

    摘要:在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用python多线程中提供Lo ...

  5. c# thread4——lock,死锁,以及monitor关键字

    多线程的存在是提高系统效率,挖掘cpu性能的一种手段,那么控制它,能够协同多个线程不发生bug是关键. 首先我们来看一段不安全的多线程代码. public abstract class Calcula ...

  6. c#多线程中Lock()关键字的用法小结

    本篇文章主要是对c#多线程中Lock()关键字的用法进行了详细的总结介绍,需要的朋友可以过来参考下,希望对大家有所帮助     本文介绍C# lock关键字,C#提供了一个关键字lock,它可以把一段 ...

  7. c#语言-多线程中的锁系统(一)

    介绍 平常在多线程开发中,总避免不了线程同步.本篇就对net多线程中的锁系统做个简单描述.   目录 一:lock.Monitor        1:基础.        2: 作用域.       ...

  8. 多线程-synchronized、lock

    1.什么时候会出现线程安全问题? 在多线程编程中,可能出现多个线程同时访问同一个资源,可以是:变量.对象.文件.数据库表等.此时就存在一个问题: 每个线程执行过程是不可控的,可能导致最终结果与实际期望 ...

  9. C#中的lock关键字

    前几天与同事激烈讨论了一下,有一点收获,记录起来. 首先给出MSDN的定义: lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断.这是通过在代码块运行期间为给定对象获取互斥锁来实现的. ...

随机推荐

  1. 中英文对照 —— 标点符号(punctuation)

    有限的几个: What Are the Fourteen Punctuation Marks in English Grammar? period:句号:comma:逗号:冒号:colon:分号:se ...

  2. 【例题 6-19 UVA - 1572】Self-Assembly

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 旋转和翻转,会发现. 如果可以顺着某个方向一直放的话. 总是能转换成往下或者往右连的. 则只要能够出现一个连接顺序的循环,则总是有解 ...

  3. spring接收对象数组实例

    JS var param= new Array(); var one= new Object; one.id = '1'; one.name= 'simba1'; param.push(one); v ...

  4. 深度学习 Deep Learning UFLDL 最新Tutorial 学习笔记 4:Debugging: Gradient Checking

    1 Gradient Checking 说明 前面我们已经实现了Linear Regression和Logistic Regression.关键在于代价函数Cost Function和其梯度Gradi ...

  5. 【微信小程序】自定义模态框实例

    原文链接:https://mp.weixin.qq.com/s/23wPVFUGY-lsTiQBtUdhXA 1 概述 由于官方API提供的显示模态弹窗,只能简单地显示文字内容,不能对对话框内容进行自 ...

  6. 王立平--eclipse本地配置svn

    1.下载 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMzQyNTUyNw==/font/5a6L5L2T/fontsize/400/fill/I ...

  7. 《Unix编程艺术》读书笔记(1)

    <Unix编程艺术>读书笔记(1) 这两天開始阅读该书,以下是自己的体会,以及原文的摘录,尽管有些东西还无法全然吃透. 写优雅的代码来提高软件系统的透明性:(P134) Elegance ...

  8. ios开发多线程四:NSOperation多图下载综合案例

    #import "ViewController.h" #import "XMGAPP.h" @interface ViewController () /** t ...

  9. C语言之基本算法11—牛顿迭代法求平方根

    //迭代法 /* ================================================================== 题目:牛顿迭代法求a的平方根!迭代公式:Xn+1 ...

  10. com.octo.captcha.service.CaptchaServiceException: Invalid ID, could not validate unexisting o

    <p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px;"& ...