条件变量c# /. net

本文关键字:net 变量 条件 | 更新日期: 2023-09-27 18:06:18

在构建条件变量类的过程中,我偶然发现了一种非常简单的方法,我想与堆栈溢出社区分享这一方法。我在谷歌上搜索了将近一个小时,但没有找到一个好的教程或。net的例子,希望这能对其他人有用。

条件变量c# /. net

一旦你知道了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类来实现条件变量来构建事件驱动消息系统是多么简单。希望你觉得这有用。

使用ManualResetEvent

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

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

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

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

在使用Monitor时可以遵循一个更好的模式。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,我认为我的解决方案抓住了需求& &;定义比你的更精确。具体来说,当且仅当队列中没有剩余物品时,应该强制消费者等待;否则就取决于操作系统了。Net运行时来调度线程。然而,在你的解决方案中,消费者被迫在每个循环中等待,而不管它是否实际消耗了任何东西——这就是我所说的过度等待/上下文切换。

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

3、你的解决方案在锁范围内发出信号,而我的解决方案在锁范围外发出信号。请参考这个答案,为什么你的解决方案更差。为什么我们要在锁作用域之外发出信号

我正在谈论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);
}

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

好消息是。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();
  }
}

下面是一个使用100%托管代码的c#条件变量类的完整源代码的链接。

https://github.com/CodeExMachina/ConditionVariable

我想我找到了解决典型问题的方法

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 . evtloglistfill . set ();可以释放program . evtloglistfill . waitone()上的锁,也可以释放下一个锁。

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