16

在我构建条件变量类的过程中,我偶然发现了一种非常简单的方法,我想与堆栈溢出社区分享这一点。我在谷歌上搜索了一个小时的大部分时间,但实际上找不到合适的教程或 .NET-ish 示例,希望这对其他人有用。

4

6 回答 6

27

一旦你了解了 and 的语义,它实际上非常lock简单Monitor

但首先,您确实需要一个对象引用。您可以使用this,但请记住,任何引用您的类的人都可以锁定该引用thispublic如果您对此感到不舒服,可以创建一个新的私有引用,如下所示:

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.PulseAllMonitor.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)。Monitor重点是说明使用.NET 中的类来实现条件变量来构建事件驱动的消息系统是多么简单。希望您觉得这个有帮助。

于 2013-03-27T11:22:52.750 回答
7

使用 ManualResetEvent

与条件变量类似的类是ManualResetEvent,只是方法名略有不同。

notify_one()C++ 中将Set()在 C# 中命名。
wait()C++ 中将WaitOne()在 C# 中命名。

此外,ManualResetEvent还提供了Reset()将事件状态设置为无信号的方法。

于 2017-08-29T10:11:52.887 回答
5

公认的答案不是一个好答案。根据 Dequeue() 代码,Wait() 在每个循环中都会被调用,这会导致不必要的等待,从而导致过多的上下文切换。正确的范例应该是,当满足等待条件时调用 wait() 。在这种情况下,等待条件是 q.Count() == 0。

在使用监视器时,这是一个更好的模式。 https://msdn.microsoft.com/en-us/library/windows/desktop/ms682052%28v=vs.85%29.aspx

关于 C# Monitor 的另一条评论是,它不使用条件变量(这实际上会唤醒所有等待该锁的线程,而不管它们等待的条件如何;因此,一些线程可能会抓住锁并立即当他们发现等待条件没有改变时返回睡眠)。它不像 pthread 那样为您提供查找粒度的线程控制。但无论如何它都是.Net,所以并不完全出乎意料。

=============应约翰的要求,这是一个改进的版本=============

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;
    }
}

我想指出几点:

1,我认为我的解决方案比您的解决方案更准确地捕获了需求和定义。具体来说,当且仅当队列中没有任何东西时,消费者才应该被迫等待;否则由 OS/.Net 运行时来安排线程。然而,在您的解决方案中,消费者被迫在每个循环中等待,无论它是否实际消耗了任何东西——这就是我所说的过度等待/上下文切换。

2,我的解决方案是对称的,即消费者和生产者代码共享相同的模式,而您的则不是。如果您确实知道模式并且只是在这种特殊情况下省略了,那么我收回这一点。

3,您的解决方案在锁定范围内发出信号,而我的解决方案在锁定范围外发出信号。请参阅此答案,了解为什么您的解决方案更糟。 为什么我们要在锁定范围之外发出信号

我说的是C#监视器中缺少条件变量的缺陷,这里是它的影响:C#根本没有办法实现将等待线程从条件队列移动到锁队列的解决方案。因此,在链接中答案提出的三线程场景中,注定会发生过度的上下文切换。

此外,缺少条件变量使得无法区分线程等待相同共享资源/锁的各种情况,但原因不同。所有等待线程都放置在该共享资源的大等待队列中,这会降低效率。

“但无论如何它都是.Net,所以并不完全出乎意料”——.Net 没有像 C++ 那样追求高效率是可以理解的,这是可以理解的。但这并不意味着程序员不应该知道这些差异及其影响。

于 2015-01-26T16:59:51.823 回答
4

转到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);
}
于 2016-03-18T17:40:12.527 回答
1

正如 h9uest 的回答和评论所指出的那样,监视器的等待接口不允许适当的条件变量(即它不允许在每个共享锁上等待多个条件)。

好消息是.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% 托管代码的适当条件变量类的完整源代码的链接。

https://github.com/CodeExMachina/ConditionVariable

于 2016-05-11T13:13:13.217 回答
0

我想我在一个典型的问题上找到了“The WAY”

List<string> log; 

由多个线程使用,一个填充它,另一个处理,另一个清空

避空

    while(true){
    //stuff
    Thread.Sleep(100)
    }

程序中使用的变量

    public static readonly List<string> logList = new List<string>();

    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{}

    }

此解决方案经过全面测试,指令 Program.evtLogListFilled.Set(); 可能会释放 Program.evtLogListFilled.WaitOne() 上的锁定以及下一个未来锁定。

我认为这是最简单的方法。

于 2017-06-07T13:29:57.330 回答