ManualResetEventSlim:调用.Set()后紧跟.Reset()不会释放*任何*等待线程

时间:2013-02-25 12:49:35

标签: c# multithreading

ManualResetEventSlim:调用.Set()后紧跟.Reset()不会释放任何等待线程

(注意:这也适用于ManualResetEvent,而不仅仅是ManualResetEventSlim。)

我在发布和调试模式下都尝试了以下代码。 我在四核处理器上运行的Windows 7 64位上使用.Net 4运行32位版本。 我是从Visual Studio 2012编译的(所以安装了.Net 4.5)。

我在系统上运行时的输出是:

Waiting for 20 threads to start
Thread 1 started.
Thread 2 started.
Thread 3 started.
Thread 4 started.
Thread 0 started.
Thread 7 started.
Thread 6 started.
Thread 5 started.
Thread 8 started.
Thread 9 started.
Thread 10 started.
Thread 11 started.
Thread 12 started.
Thread 13 started.
Thread 14 started.
Thread 15 started.
Thread 16 started.
Thread 17 started.
Thread 18 started.
Thread 19 started.
Threads all started. Setting signal now.

0/20 threads received the signal.

因此设置然后立即重置事件并没有释放单个线程。如果你取消注释Thread.Sleep(),那么它们都会被释放。

这似乎有些出乎意料。

有没有人有解释?

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    public static class Program
    {
        private static void Main(string[] args)
        {
            _startCounter = new CountdownEvent(NUM_THREADS); // Used to count #started threads.

            for (int i = 0; i < NUM_THREADS; ++i)
            {
                int id = i;
                Task.Factory.StartNew(() => test(id));
            }

            Console.WriteLine("Waiting for " + NUM_THREADS + " threads to start");
            _startCounter.Wait(); // Wait for all the threads to have called _startCounter.Signal() 
            Thread.Sleep(100); // Just a little extra delay. Not really needed.
            Console.WriteLine("Threads all started. Setting signal now.");
            _signal.Set();
            // Thread.Sleep(50); // With no sleep at all, NO threads receive the signal.
            _signal.Reset();
            Thread.Sleep(1000);
            Console.WriteLine("\n{0}/{1} threads received the signal.\n\n", _signalledCount, NUM_THREADS);
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }

        private static void test(int id)
        {
            Console.WriteLine("Thread " + id + " started.");
            _startCounter.Signal();
            _signal.Wait();
            Interlocked.Increment(ref _signalledCount);
            Console.WriteLine("Task " + id + " received the signal.");
        }

        private const int NUM_THREADS = 20;

        private static readonly ManualResetEventSlim _signal = new ManualResetEventSlim();
        private static CountdownEvent _startCounter;
        private static int _signalledCount;
    }
}

注意:这个问题提出了类似的问题,但它似乎没有答案(除了确认是,这可能发生)。

Issue with ManualResetEvent not releasing all waiting threads consistently


[编辑]

正如Ian Griffiths在下面指出的那样,答案是使用的基础Windows API不支持此功能。

令人遗憾的是the Microsoft documentation for ManualResetEventSlim.Set()错误地指出它

  

设置要发信号的事件的状态,允许一个或多个   等待事件继续进行的线程。

显然,“一个或多个”应为“零或更多”。

1 个答案:

答案 0 :(得分:10)

重置ManualResetEvent与调用Monitor.Pulse不同 - 它无法保证它会释放任何特定数量的线程。相反,文档(对于底层的Win32同步原语)非常清楚,你不知道会发生什么:

  

当对象的状态发出信号时,可以释放任意数量的等待线程或随后开始对指定事件对象执行等待操作的线程

这里的关键词是“任何数字”,包括零。

Win32确实提供了PulseEvent,但正如它所说“此功能不可靠且不应使用”。在http://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx的文档中的评论提供了一些深入了解为什么使用事件对象无法可靠地实现脉冲样式语义的原因。 (基本上,内核有时需要暂时等待事件等待事件的线程,因此线程总是可能错过事件的'脉冲'。无论你使用PulseEvent还是尝试,都是如此通过设置和重置事件来自己完成。)

ManualResetEvent的预期语义是它充当门。设置时门打开,重置时门关闭。如果你打开一个大门然后在有人有机会通过大门之前快速关闭它,如果每个人仍然在大门的错误一侧,你不应该感到惊讶。只有那些在你打开门时足够警觉才能通过大门的人才能通过。这就是它的工作方式,这就是为什么你看到你所看到的。

特别是Set的语义非常“打开门,并确保所有等待的线程都通过门”。 (如果它意味着,内核应该对多对象等待做什么并不明显。)所以这不是一个“问题”,因为事件不是以你的方式使用试图使用它,所以它正常运作。但从某种意义上说,这是一个问题,你将无法使用它来获得你正在寻找的效果。 (这是一个有用的原语,它对你正在尝试做的事情没用。我倾向于将ManualResetEvent专门用于最初关闭的门,并且只打开一次,并且永远不会再次关闭。)

所以你可能需要考虑其他一些同步原语。