在我寻求建立一个条件变量类我偶然发现了一个简单的方法,我想与堆栈溢出社区分享这个。我在一个小时的大部分时间里搜索,无法真正找到一个好的教程或.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
的锁定协议是这样的,它将在Monitor.Wait
期间释放锁。实际上,要求您在调用Monitor.Pulse
,Monitor.PulseAll
或Monitor.Wait
之前获得锁。
您应该了解这种方法的一个警告。由于在调用Monitor
的通信方法之前需要保持锁定,因此您应该只在尽可能短的时间内保持锁定。RunLoop
的变体对长时间运行的后台任务更友好,如下所示:
void RunLoop()
{
for (;;)
{
// do work here...
lock (syncPrimitive)
{
Monitor.Wait(syncPrimitive);
}
}
}
但是现在我们已经改变了这个问题,因为锁在整个处理过程中不再保护共享资源。因此,如果do work here...
位中的某些代码需要访问共享资源,则需要一个单独的锁来管理对该资源的访问。
我们可以利用上面的内容创建一个简单的线程安全的生产者使用者集合(尽管.NET 已经提供了一个优秀的ConcurrentQueue<T>
实现;这只是为了说明在实现这样的机制时使用Monitor
的简单性)。
class BlockingQueue<T>
{
// We base our queue on the (non-thread safe) .NET 2.0 Queue collection
readonly Queue<T> q = new Queue<T>();
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 那样为您提供查找粒度的线程控制。
= = = = = = = = = = = = = 根据 John 的要求,这是一个改进的版本 = = = = = = = = = = = = = = =
class BlockingQueue<T>
{
readonly Queue<T> q = new Queue<T>();
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;
}
}
我想指出的几件事:
然而,在您的解决方案中,消费者在每个循环中等待,无论它是否实际消耗了任何东西-这就是我所说的过度等待 / 上下文切换。
2,我的解决方案是对称的,因为消费者和生产者代码共享相同的模式,而你的不是。
3,您的解决方案在锁范围内发出信号,而我的解决方案在锁范围外发出信号。请参考此答案,以了解您的解决方案为何更差。why should we signal outside the lock scope
我说的是 C # 监视器中缺少条件变量的缺陷,这里是它的影响: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);
}
本站系公益性非盈利分享网址,本文来自用户投稿,不代表码文网立场,如若转载,请注明出处
评论列表(71条)