C# Monitor Wait()和Pulse()

 

1.Monitor.Wait方法
当线程调用 Wait 时,它释放对象的锁并进入对象的等待队列,对象的就绪队列中的下一个线程(如果有)获取锁并拥有对对象的独占使用。Wait()就是交出锁的使用权,使线程处于阻塞状态,直到再次获得锁的使用权。

2.Monitor.Pulse方法
当前线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。pulse()并不会使当前线程释放锁。

简述:

共用同一lock对象两线程不能只调用Wait(),Wait这个方法反而放弃了锁的使用权,同时阻塞当前线程,线程就直接休眠(进入WaitSleepJoin状态),同时在主线程中Join这个work线程时,也就一直不能返回了。线程将一直阻塞。

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

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

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

Assembly code
|- 拥有锁的线程 lockObj->|- 就绪队列(ready queue) |- 等待队列(wait queue)

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

比如下面的程序:

C# code
class Program { staticvoid Main(string[] args) { new Thread(A).Start(); new Thread(B).Start(); new Thread(C).Start(); Console.ReadLine(); } staticobject lockObj =newobject(); staticvoid A() { lock (lockObj) //进入就绪队列 { Thread.Sleep(1000); Monitor.Pulse(lockObj); Monitor.Wait(lockObj); //自我流放到等待队列 } Console.WriteLine("A exit..."); } staticvoid B() { Thread.Sleep(500); lock (lockObj) //进入就绪队列 { Monitor.Pulse(lockObj); } Console.WriteLine("B exit..."); } staticvoidC() { Thread.Sleep(800); lock (lockObj) //进入就绪队列 { } Console.WriteLine("C exit..."); } }

从时间线上来分析:

Assembly code
T 线程A 0lock( lockObj ) 1 { 2 //... 线程B 线程C 3 //... lock( lockObj ) lock( lockObj ) 4 //... { { 5 //... //... 6 //... //... 7Monitor.Pulse //... 8Monitor.Wait //... 9 //... Monitor.Pulse 10 //... } } 11 } 时间点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...
 
由于Monitor.Wait的暂时放弃和Monitor.Pulse的开门机制,我们可以用Monitor来实现更丰富的同步机制,比如一个事件机(ManualResetEvent):
C# code
class MyManualEvent { privateobject lockObj =newobject(); privatebool hasSet =false; publicvoid Set() { lock (lockObj) { hasSet =true; Monitor.PulseAll(lockObj); } } publicvoid WaitOne() { lock (lockObj) { while (!hasSet) { Monitor.Wait(lockObj); } } } } class Program { static MyManualEvent myManualEvent =new MyManualEvent(); staticvoid Main(string[] args) { ThreadPool.QueueUserWorkItem(WorkerThread, "A"); ThreadPool.QueueUserWorkItem(WorkerThread, "B"); Console.WriteLine("Press enter to signal the green light"); Console.ReadLine(); myManualEvent.Set(); ThreadPool.QueueUserWorkItem(WorkerThread, "C"); Console.ReadLine(); } staticvoidWorkerThread(object state) { myManualEvent.WaitOne(); Console.WriteLine("Thread {0} got the green light...", state); } }

我们看到了该玩具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实际上一个语法糖糖,C#编译器实际上把他展开为Monitor.Enter和Monitor.Exit,即:

C# code
lock(lockObj) { //...} ////相当于(.Net4以前):Monitor.Enter(lockObj); try { //...} finally { Monitor.Exit(lockObj); }

但是,这种实现逻辑至少理论上有一个错误:当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:

C# code
lock(lockObj) { //...} ////相当于(DotNet 4):bool lockTaken =false; try { Monitor.Enter(lockObj,ref lockTaken); // } finally { if (lockTaken) Monitor.Exit(lockObj); }

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

注释和引用:
Monitor.Wait 方法
http://msdn.microsoft.com/zh-cn/library/79fkfcw1.aspx

Monitor.TryEnter 方法
http://msdn.microsoft.com/zh-cn/library/dd289679.aspx

请问,多线程Monitor类
http://topic.csdn.net/u/20111206/15/744c70de-49dc-4694-a09e-180438d7f8f0.html

请问,这个关于多线程的代码不懂
http://topic.csdn.net/u/20111208/23/64671dd4-7fdc-4d76-b3b9-1fd18087e6e0.html

C# Monitor Wait()和Pulse()的更多相关文章

  1. 多线程中的lock,Monitor.Wait和Monitor.Pulse

    我们知道lock实际上一个语法糖糖,C#编译器实际上把他展开为Monitor.Enter和Monitor.Exit,即: lock(lockObj) { //... } ////相当于(.Net4以前 ...

  2. C#多线程的介绍(园子里比较全的一篇)

    一.多线程的概念  Windows是一个多任务的系统,如果你使用的是windows 2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程.什么是进程呢?当一个程序开始运行时,它就是一 ...

  3. 【CLR VIA C#】读书笔记

    工作几年了才看,记录下笔记备忘. 章节 笔记 1.CLR的执行模型 公共语言运行时(Common Language Runtime,CLR) 源代码-->编译器检查语法和分析源代码-->托 ...

  4. C# 多线程控制

    C# 多线程控制 通讯 和切换   一.多线程的概念  Windows是一个多任务的系统,如果你使用的是windows 2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程.什么是进 ...

  5. C# 多线程控制 通讯

    一.多线程的概念  Windows是一个多任务的系统,如果你使用的是windows 2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程.什么是进程呢?当一个程序开始运行时,它就是一 ...

  6. C# 多线程控制 通讯 和切换

    一.多线程的概念  Windows是一个多任务的系统,如果你使用的是windows 2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程.什么是进程呢?当一个程序开始运行时,它就是一 ...

  7. C#多线程代码示例

    using System; using System.Threading; namespace MultiThreadDemo { class Program { public static void ...

  8. C# 篇基础知识10——多线程

    1.线程的概念 单核CPU的计算机中,一个时刻只能执行一条指令,操作系统以“时间片轮转”的方式实现多个程序“同时”运行.操作系统以进程(Process)的方式运行应用程序,进程不但包括应用程序的指令流 ...

  9. C# Monitor的Wait和Pulse方法使用详解

    [转载]http://blog.csdn.net/qqsttt/article/details/24777553 Monitor的Wait和Pulse方法在线程的同步锁使用中是比较复杂的,理解稍微困难 ...

随机推荐

  1. 【译】优雅的停止docker容器

    1. 介绍 Docker的大部分重点是在隔离的容器中打包和运行应用程序的过程.有无数的教程说明了如何在Docker容器中运行应用程序,但是很少有教程讨论如何正确停止容器化的应用程序.这似乎是一个愚蠢的 ...

  2. Redis高级特性及应用场景

    Redis高级特性及应用场景 redis中键的生存时间(expire) redis中可以使用expire命令设置一个键的生存时间,到时间后redis会自动删除它. 过期时间可以设置为秒或者毫秒精度. ...

  3. 《计算机系统要素》第四章 类汇编语言 Hack

    这章通过学习书中自己设计的Hack语言的使用,弄懂汇编语言的工作原理. 汇编语言最接近底层了,因为每个指令对应一个二进制编码. 当这些指令都变成...0101011100101...的形式后,内存Me ...

  4. Docker容器时间与主机时间相差8小时

    查看主机时间 [root@localhost ~]# date 2016年 07月 27日 星期三 22:42:44 CST 查看容器时间 root@b43340ecf5ef:/# date Wed ...

  5. jmeter之吞吐量控制器

    比如说有一种场景是,10个并发里,有2个事操作业务A,有8个是操作业务B,要模拟这种业务场景,则可以通过吞吐量控制器来模拟 目录 1.用法 2.举例 1.用法 第一种:设置比例控制 选择percent ...

  6. Kafka管理与监控——调优

    1.JVM参数配置优化 如果使用的CMS GC算法,建议JVM Heap不要太大,在4GB以内就可以.JVM太大,导致Major GC或者Full GC产生的“stop the world”时间过长, ...

  7. DP————最小覆盖问题

    原题:https://www.luogu.org/problem/P2279 题解转载自:https://www.luogu.org/blog/contributation/solution-p227 ...

  8. MySQL知识篇-SQL1

    1 SQL是什么? 答:是结构话语言,是一种操作关系型数据库的语言. 2 SQL语言分类? SQL语言 说明 举例 DDL 数据定义语言 create  drop DML 数据操作语言 insert ...

  9. 【计算机视觉】极限优化:Haar特征的另一种的快速计算方法—boxfilter

    这种以Boxfilter替代integral image 的方法很难使用到haar.LBP等特征检测中,因为像下面说的,它不支持多尺度,也就是说所提取的特征必须是同一个大小,最起码同一个宽高比的,这一 ...

  10. Go语言中的打包和工具链

    包 所有Go语言的程序都会组织成若干组文件,每组文件被称为一个包.这样每个包的代码都可以作为很小的复用单元,被其他项目引用. 包名惯例 给包命名的惯例是使用包所在目录的名字.并不需要所有包的名字都与别 ...