条件变量C#/。NET
在我构建条件变量类的过程中,我偶然发现了一个简单的方法,我想与堆栈溢出社区分享。 我正在谷歌搜索一小时的大部分时间,并且无法找到一个好的教程或.NET-ish示例感觉正确,希望这对其他人有用。
一旦你了解了lock
和Monitor
的语义,它实际上非常简单。
但首先,您需要一个对象引用。 您可以使用this
,但请记住, this
是public
,因为任何引用您的类的人都可以锁定该引用。 如果您对此感到不舒服,可以创建一个新的私有引用,如下所示:
readonly object syncPrimitive = new object(); // this is legal
在代码中您希望能够提供通知的某处,可以像这样完成:
void Notify() { lock (syncPrimitive) { Monitor.Pulse(syncPrimitive); } }
你做实际工作的地方是一个简单的循环结构,如下所示:
void RunLoop() { lock (syncPrimitive) { for (;;) { // do work here... Monitor.Wait(syncPrimitive); } } }
从外面看,这看起来非常糟糕,但是当你调用Monitor.Wait
, Monitor
的锁定协议会释放锁定,实际上,你需要在调用Monitor.Pulse
, Monitor.PulseAll
之前获得锁定Monitor.PulseAll
或Monitor.Wait
。
您应该了解这种方法的一个警告。 由于在调用Monitor
的通信方法之前需要保持锁定,因此您应该只在尽可能短的时间内挂起锁定。 对于长时间运行的后台任务更友好的RunLoop
的变体看起来像这样:
void RunLoop() { for (;;) { // do work here... lock (syncPrimitive) { Monitor.Wait(syncPrimitive); } } }
但是现在我们已经改变了一点问题,因为锁不再用于保护共享资源,因此,如果你do work here...
代码的代码do work here...
需要访问共享资源,你需要额外的锁,保护该资源。
我们可以利用上面的代码创建一个简单的线程安全的生产者消费者集合,虽然.NET已经提供了一个优秀的ConcurrentQueue
实现,这只是为了说明使用Monitor
的简单性。
class BlockingQueue { // We base our queue, on the non-thread safe // .NET 2.0 queue collection readonly Queue q = new Queue (); public void Enqueue(T item) { lock (q) { q.Enqueue(item); System.Threading.Monitor.Pulse(q); } } public T Dequeue() { lock (q) { for (; ; ) { if (q.Count > 0) { return q.Dequeue(); } System.Threading.Monitor.Wait(q); } } } }
现在,重点是不要构建一个阻塞集合,它也可以在.NET框架中使用(参见BlockingCollection)。 关键是要说明使用.NET中的Monitor
类来实现条件变量来构建事件驱动的消息系统是多么简单。 希望您觉得这个有帮助。
使用ManualResetEvent
与条件变量类似的类是ManualResetEvent ,只是方法名称略有不同。
C ++中的notify_one()
将在C#中命名为Set()
。
C ++中的wait()
将在C#中命名为WaitOne()
。
此外, ManualResetEvent还提供了一个Reset()
方法,用于将事件的状态设置为无信号。
接受的答案并不好。 根据Dequeue()代码,在每个循环中调用Wait(),这会导致不必要的等待,从而导致过多的上下文切换。 应该是正确的范例,当满足等待条件时调用wait()。 在这种情况下,等待条件是q.Count()== 0。
在使用监视器时,这是一个更好的模式。 https://msdn.microsoft.com/en-us/library/windows/desktop/ms682052%28v=vs.85%29.aspx
关于C#Monitor的另一个评论是,它没有使用条件变量(它基本上会唤醒等待该锁的所有线程,无论它们等待的条件如何;因此,某些线程可能会立即获取锁定当他们发现等待条件没有改变时返回睡眠状态)。 它没有为您提供像pthreads一样的查找粒度线程控制。 但无论如何它都是.Net,所以并非完全出乎意料。
=============根据约翰的要求,这是一个改进版本=============
class BlockingQueue { readonly Queue q = new Queue (); public void Enqueue(T item) { lock (q) { while (false) // condition predicate(s) for producer; can be omitted in this particular case { System.Threading.Monitor.Wait(q); } // critical section q.Enqueue(item); } // generally better to signal outside the lock scope System.Threading.Monitor.Pulse(q); } public T Dequeue() { T t; lock (q) { while (q.Count == 0) // condition predicate(s) for consumer { System.Threading.Monitor.Wait(q); } // critical section t = q.Dequeue(); } // this can be omitted in this particular case; but not if there's waiting condition for the producer as the producer needs to be woken up; and here's the problem caused by missing condition variable by C# monitor: all threads stay on the same waiting queue of the shared resource/lock. System.Threading.Monitor.Pulse(q); return t; } }
我想指出的一些事情:
1,我认为我的解决方案比您的解决方案更精确地捕获了需求和定义。 具体来说,当且仅当队列中没有任何东西时,应该强迫消费者等待; 否则由OS / .Net运行时来计划线程。 然而,在您的解决方案中,消费者被迫在每个循环中等待,无论它是否实际消耗了任何东西 – 这是我所讨论的过多的等待/上下文切换。
2,我的解决方案是对称的,即消费者和生产者代码共享相同的模式,而你的解决方案不是。 如果您确实知道该模式并且仅针对此特定情况省略,那么我会回过头来说明这一点。
3,您的解决方案在锁定范围内发出信号,而我的解决方案在锁定范围外发出信号。 请参考这个答案,了解为什么你的解决方案更糟糕。 我们为什么要在锁定范围外发出信号
我在讨论C#monitor中缺少条件变量的缺陷,这就是它的影响:C#根本无法实现将等待线程从条件队列移动到锁定队列的解决方案。 因此,过多的上下文切换注定要在链接中的答案提出的三线程场景中发生。
此外,缺少条件变量使得无法区分线程在同一共享资源/锁上等待的各种情况,但出于不同的原因。 所有等待的线程都放在该共享资源的大等待队列中,这会破坏效率。
“但无论如何,它都是.Net,所以并非完全出乎意料” – 可以理解.Net并不像C ++那样追求高效率,这是可以理解的。 但这并不意味着程序员不应该知道差异及其影响。
转到deadlockempire.github.io/ 。 他们有一个惊人的教程,可以帮助您了解条件变量和锁定,并将有助于您编写所需的课程。
您可以在deadlockempire.github.io中逐步执行以下代码并进行跟踪。 这是代码片段
while (true) { Monitor.Enter(mutex); if (queue.Count == 0) { Monitor.Wait(mutex); } queue.Dequeue(); Monitor.Exit(mutex); } while (true) { Monitor.Enter(mutex); if (queue.Count == 0) { Monitor.Wait(mutex); } queue.Dequeue(); Monitor.Exit(mutex); } while (true) { Monitor.Enter(mutex); queue.Enqueue(42); Monitor.PulseAll(mutex); Monitor.Exit(mutex); }
正如h9uest的回答和注释所指出的那样,Monitor的Wait接口不允许正确的条件变量(即它不允许每个共享锁等待多个条件)。
好消息是.NET中的其他同步原语(例如SemaphoreSlim,lock关键字,Monitor.Enter / Exit)可用于实现适当的条件变量。
以下ConditionVariable类将允许您使用共享锁在多个条件上等待。
class ConditionVariable { private int waiters = 0; private object waitersLock = new object(); private SemaphoreSlim sema = new SemaphoreSlim(0, Int32.MaxValue); public ConditionVariable() { } public void Pulse() { bool release; lock (waitersLock) { release = waiters > 0; } if (release) { sema.Release(); } } public void Wait(object cs) { lock (waitersLock) { ++waiters; } Monitor.Exit(cs); sema.Wait(); lock (waitersLock) { --waiters; } Monitor.Enter(cs); } }
您需要做的就是为您希望能够等待的每个条件创建ConditionVariable类的实例。
object queueLock = new object(); private ConditionVariable notFullCondition = new ConditionVariable(); private ConditionVariable notEmptyCondition = new ConditionVariable();
然后就像在Monitor类中一样,必须从同步的代码块中调用ConditionVariable的Pulse和Wait方法。
T Take() { lock(queueLock) { while(queue.Count == 0) { // wait for queue to be not empty notEmptyCondition.Wait(queueLock); } T item = queue.Dequeue(); if(queue.Count < 100) { // notify producer queue not full anymore notFullCondition.Pulse(); } return item; } } void Add(T item) { lock(queueLock) { while(queue.Count >= 100) { // wait for queue to be not full notFullCondition.Wait(queueLock); } queue.Enqueue(item); // notify consumer queue not empty anymore notEmptyCondition.Pulse(); } }
下面是使用C#中100%托管代码的正确条件变量类的完整源代码的链接。
我想我找到了一个关于a的典型问题的“方法”
List log;
由多个线程使用,一个填充它和另一个处理而另一个处理
避免空虚
while(true){ //stuff Thread.Sleep(100) }
程序中使用的变量
public static readonly List logList = new List (); public static EventWaitHandle evtLogListFilled = new AutoResetEvent(false);
处理器的工作方式
private void bw_DoWorkLog(object sender, DoWorkEventArgs e) { StringBuilder toFile = new StringBuilder(); while (true) { try { { //waiting form a signal Program.evtLogListFilled.WaitOne(); try { //critical section Monitor.Enter(Program.logList); int max = Program.logList.Count; for (int i = 0; i < max; i++) { SetText(Program.logList[0]); toFile.Append(Program.logList[0]); toFile.Append("\r\n"); Program.logList.RemoveAt(0); } } finally { Monitor.Exit(Program.logList); // end critical section } try { if (toFile.Length > 0) { Logger.Log(toFile.ToString().Substring(0, toFile.Length - 2)); toFile.Clear(); } } catch { } } } catch (Exception ex) { Logger.Log(System.Reflection.MethodBase.GetCurrentMethod(), ex); } Thread.Sleep(100); } }
在我们的填充线程上
public static void logList_add(string str) { try { try { //critical section Monitor.Enter(Program.logList); Program.logList.Add(str); } finally { Monitor.Exit(Program.logList); //end critical section } //set start Program.evtLogListFilled.Set(); } catch{} }
这个解决方案经过全面测试,istruction Program.evtLogListFilled.Set(); 可以释放Program.evtLogListFilled.WaitOne()上的锁以及下一个未来的锁。
我认为这是最简单的方法。