条件变量C#/。NET

时间:2013-03-27 11:22:53

标签: c# .net

在我构建条件变量类的过程中,我偶然发现了一种简单的方法,我希望与堆栈溢出社区分享。我正在谷歌搜索一小时的大部分时间,并且无法找到一个好的教程或.NET-ish示例感觉正确,希望这对其他人有用。

6 个答案:

答案 0 :(得分:18)

一旦你了解lockMonitor的语义,它实际上非常简单。

但首先,您需要一个对象引用。您可以使用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.PulseMonitor.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)。关键在于说明使用.NET中的Monitor类来实现条件变量来构建事件驱动的消息系统是多么简单。希望你觉得这很有用。

答案 1 :(得分:4)

使用ManualResetEvent

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

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

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

答案 2 :(得分:2)

接受的答案并不好。 根据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<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,您的解决方案在锁定范围内发出信号,而我的解决方案在锁定范围外发出信号。请参阅此答案,了解您的解决方案为何更糟糕。 why should we signal outside the lock scope

我在谈论C#monitor中缺少条件变量的缺陷,以及它的影响:C#根本无法实现将等待线程从条件队列移动到锁队列。因此,过多的上下文切换注定要在链接中的答案提出的三线程场景中进行。

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

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

答案 3 :(得分:2)

转到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);
}

答案 4 :(得分:1)

正如h9uest的回答和注释所指出的那样,Monitor的Wait接口不允许正确的条件变量(即它不允许每个共享锁等待多个条件)。

好消息是.NET中的其他同步原语(例如SemaphoreSlim,lock keyword,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

答案 5 :(得分:0)

我想我找到了#34; The WAY&#34;

的典型问题
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{}

    }

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

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