条件变量C#/.NET

Joh*_*ren 11 .net c#

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

Joh*_*ren 19

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

但首先,您需要一个对象引用.您可以使用this,但请记住,thispublic在这个意义上,任何人只要有你的类的引用可以对参考锁定.如果您对此感到不舒服,可以创建一个新的私有引用,如下所示:

readonly object syncPrimitive = new object(); // this is legal
Run Code Online (Sandbox Code Playgroud)

在代码中您希望能够提供通知的某处,可以像这样完成:

void Notify()
{
    lock (syncPrimitive)
    {
        Monitor.Pulse(syncPrimitive);
    }
}
Run Code Online (Sandbox Code Playgroud)

你做实际工作的地方是一个简单的循环结构,如下所示:

void RunLoop()
{
    lock (syncPrimitive)
    {
        for (;;)
        {
            // do work here...
            Monitor.Wait(syncPrimitive);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

从外面来看,这看起来非常糟糕,但是Monitor当你打电话时Monitor.Wait,锁定协议会释放锁定,实际上,你需要在调用之前获得锁定Monitor.Pulse,Monitor.PulseAll或者Monitor.Wait.

您应该了解这种方法的一个警告.由于在调用通信方法之前需要保持锁定,因此Monitor您应该只在尽可能短的时间内挂起锁定.RunLoop对长时间运行的后台任务更友好的变体看起来像这样:

void RunLoop()
{

    for (;;)
    {
        // do work here...

        lock (syncPrimitive)
        {
            Monitor.Wait(syncPrimitive);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是现在我们已经改变了一些问题,因为锁不再用于保护共享资源,因此,如果您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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,重点是不要构建一个阻塞集合,它也可以在.NET框架中使用(参见BlockingCollection).关键在于说明使用Monitor.NET中的类来实现条件变量来构建事件驱动的消息系统是多么简单.希望您觉得这个有帮助.

  • @Nawaz在另一个回答这个问题时已经讨论过的事情.如果我们看一下源代码,就会有一个关于它的注释,但它没有说明为什么http://referencesource.microsoft.com/#mscorlib/system/threading/monitor.cs,12029e7542a2ec9c,它只是说它只是说它如果在同步代码块之外调用`Pulse`,则会抛出SynchronizationLockException. (2认同)
  • @Nawaz`SynchronizationLockException` 的文档说它应该在您不拥有锁并且操作希望您拥有时抛出。因此,它确实说明了为什么会出现这种情况。无论是框架的限制还是技术细节,我都不能说,但这绝对是必需的。 (2认同)

h9u*_*est 6

接受的答案并不是一个好的答案。根据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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我想指出几点:

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

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

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

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

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

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


Won*_*Hau 5

使用ManualResetEvent

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

notify_one()C ++中的in将以Set()C#命名。C ++中
wait()in将以WaitOne()C#命名。

而且,ManualResetEvent还提供了Reset()一种将事件状态设置为非信号状态的方法。