C#简单理解 Monitor.Wait 与 Monitor.Pulse

Posted dotNET编程大全

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#简单理解 Monitor.Wait 与 Monitor.Pulse相关的知识,希望对你有一定的参考价值。

一. 关于 Monitor 控制下线程的三个状态

1. 拥有锁的线程:先行得到锁的线程,得到锁之后,其他线程将进入就绪队列进行等待锁的释放

2. 就绪队列中的线程:等待获取锁

3. 等待队列中的线程:等待显式地被移入就绪队列

二. 方法说明和原理

1.  Monitor.Wait 方法

有两个比较常用的方法重载:

  • Monitor.Wait(Object)

  Object:等待的锁的对象

  功能:释放当前线程所占用的对象锁,并且阻塞当前的线程直到它再次拥有这个锁。

             Releases the lock on an object and blocks the current thread until it reacquires the lock.

  • Monitor.Wait(Object,Int32)

  - Object:等待的锁的对象

  - Int32:线程再次进入就绪队列的等待时长,单位毫秒

  功能:释放当前线程所占用的对象锁,并且阻塞当前的线程直到它再次拥有这个锁。如果指定的时长过去,线程将由等待队列转移到就绪队列。

            Releases the lock on an object and blocks the current thread until it reacquires the lock. If the specified time-out interval elapses, the thread enters the ready queue.

2. Monitor.Wait 方法的主要执行步骤

  • 阻塞当前的线程

  • 将这个线程移动到等待队列中

  • 释放当前的同步锁

3. Monitor.Pulse (Object)

功能:通知一个等待队列中的线程,当前锁的状态被改变。(说白了就是有一个线程可以从等待队列中被移入就绪队列)

Notifies a thread in the waiting queue of a change in the locked object's state.

4. Monitor.PulseAll(Object)

功能:通知所有的等待队列中的线程,当前锁的状态改变。(说白了就是所有的线程可以从等待队列中被移入就绪队列)

Notifies all waiting threads in the waiting queue of a change in the locked object's state.

5. Monitor.Pulse 和 Monitor.PulseAll 的使用写法:

只能由当前获得锁的线程,调用 Monitor.Pulse 和 Monitor.PluseAll 后,使等待队列中的线程转义到就绪队列。

代码一般如下:


lock(obj){ Monitor.Pulse(obj);}
lock(obj){ Monitor.PulseAll(obj);}


三. 模拟情形分析

情形一:

1. 假设有5个同时开始的线程,分别是 t1、t2、t3、t4 和 t5,它们将会同时进入一个 lock 区域,如下:

lock(obj){ Monitor.Wait(obj);}

 2. 由于线程 t1 被第一个处理,进而进入了 lock,它获得锁,此时所有线程的状态:

拥有锁的线程 t1 
就绪队列 t2、t3、t4、t5 
等待队列 

 

 

 

 

3. 假设线程 t1 运行到了 Monitor.Wait,它将会被从拥有线程锁状态移动到等待队列状态中,于此同时将会释放其拥有的锁,而其它在就绪队列中的线程将有机会获得这个锁:

拥有锁的线程
就绪队列 t2、t3、t4、t5
等待队列 t1

 

 

 

 

4. 此时假设线程 t2 获取了 t1 释放的锁,它将进入 lock 区域中,此时所有的线程状态如下:

拥有锁的线程 t2
就绪队列 t3、t4、t5
等待队列 t1

 

 

 

 

5. 接着 t2 在 lock 区域中也会执行 Monitor.Wait ,之后 t2 也会像 t1 一样进入等待队列,重复 1、2 步骤,直至所有的线程 t1、t2、t3、t4、t5 都进入等待队列,如下图:

拥有锁的线程
就绪队列
等待队列 t1、t2、t3、t4、t5

 

 

 

 

情形二:

如何将上面等待队列中的某一个线程重新变为就绪状态,从而可以再次拿到锁呢?

答:我们可以使用 Monitor.Pulse 来让 t1 线程从等待队列中转移到就绪队列中。

★★ 这里有一个需要注意的地方,就是 " 等待队列 " 是一个队列,满足  " 先进先出 ",所以第一个线程 t1 会被优先释放到就绪队列中。如图是在情形一所有线程都在等待队列中,优先释放哪一个到就绪队列的截图:

1. 我们在情形一第5点的状态下执行 Monitor.Pulse,此时所有的线程的状态如下:

拥有锁的线程
就绪队列 t1
等待队列 t2、t3、t4、t5

 

 

 

 

2. 然后,线程 t1 在就绪队列中就会拿到锁,从 Monitor.Wait 的下一句程序开始执行:

拥有锁的线程 t1
就绪队列
等待队列 t2、t3、t4、t5

 

 

 

 

3. 最后,t1 线程在执行完 lock 区域的剩余部分的代码之后就会退出,同时释放线程锁。于此同时,其它的线程依然被卡在等待队列中等待,如下:

拥有锁的线程
就绪队列
等待队列 t2、t3、t4、t5

 

 

 

 

 4. 对于 Monitor.PulseAll 将会把所有的等待状态的线程都移到就绪状态的队列中,从而有机会获得锁进行执行。从第3步接着执行 Monitor.PulseAll 之后,所有的线程状态如下:

拥有锁的线程
就绪队列 t2、t3、t4、t5
等待队列

 

 

 

 


四. 运用

我们来利用 Monitor.Wait 和 Monitor.Pulse 来实现一下 AutoResetEvent 。

代码部分:


/// <summary>/// 自己的写的AutoResetEvent/// </summary>public class AutoResetEventEx{ /// <summary> /// 内部的设置状态 /// true 不等待信号 /// false 等待信号 /// </summary> private bool _initialState = false;
/// <summary> /// 内部锁 /// </summary> private object _objLock = new object();
/// <summary> /// 构造函数 /// </summary> /// <param name="initialState">内部的设置状态</param> public AutoResetEventEx(bool initialState) { this._initialState = initialState; }
/// <summary> /// 等待一个信号 /// </summary> public void WaitOne() { if(!this._initialState) { lock(this._objLock) { Monitor.Wait(this._objLock); } } }
/// <summary> /// 发送一个信号 /// </summary> public void Set() { if (!this._initialState) { this._initialState = true;
lock (this._objLock) { Monitor.PulseAll(this._objLock); } } }}


上端调用部分:


AutoResetEventEx autoResetEventEx = new AutoResetEventEx(true);
Thread th = new Thread(() => { Thread.Sleep(10 * 1000);
Console.WriteLine("- Set -"); autoResetEventEx.Set(); });th.IsBackground = true;th.Start();
Console.WriteLine("- WaitOne -");autoResetEventEx.WaitOne();
Console.WriteLine("- Exit -");Console.ReadKey();


运行结果:


五. 性能对比

最后,对比一下 C# 框架的 AutoResetEvent 和手动实现的 AutoResetEventEx:

  • AutoResetEventEx 是 Monitor 实现的,Monitor 采用的是混合锁(用户模式 + 内核模式),不是采用 Win32 API

  • AutoResetEvent 采用的是 Win32 的 API 的 WaitHandle 实现的,所以性能相对比较低(?)


以上是关于C#简单理解 Monitor.Wait 与 Monitor.Pulse的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Monitor.Wait() 和 Monitor.Pulse() 需要锁?

Monitor.Wait,条件变量

Monitor.Wait 是不是确保重新读取字段?

如何通知解锁线程(Monitor.Wait(),PulseAll()模拟)

lock与monitor的区别

C#互斥访问临界资源.lock 怎么超时