【问题标题】:ManualResetEventSlim: Calling .Set() followed immediately by .Reset() doesn't release *any* waiting threadsManualResetEventSlim:立即调用 .Set() 后跟 .Reset() 不会释放*任何*等待线程
【发布时间】:2013-02-10 14:53:10
【问题描述】:

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() 错误地声明它

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

显然“一个或多个”应该是“零个或多个”。

【问题讨论】:

  • 好的,我试了一下。没有睡眠,一个线程接收到信号。使用 Sleep(0),8 个线程,使用 Sleep(10),所有 20 个线程。所以,症状得到确认。
  • 您只是使用了错误的同步对象,检测信号等待句柄需要操作系统线程调度程序运行。 Slim 版本更糟糕,它很苗条,因为它不调用操作系统。您在这里需要 Monitor.Pulse(),Monitor 类会一直跟踪等待的线程。 MRE 完全缺少的功能,因为它不可能提供公平保证。但是您已经从链接的副本中知道了这一点。
  • 当然,调度程序确实在运行 - Set() 是一个系统调用。
  • 是的,我在我的实际代码中使用了 Montor.PulseAll()(不是 Monitor.Pulse())。我只是想知道为什么它不起作用。
  • “胖”ManualResetEvent 出现同样的问题。

标签: c# multithreading


【解决方案1】:

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

任何数量的等待线程,或随后开始等待指定事件对象的线程,都可以在对象状态发出信号时释放

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

Win32 确实提供了PulseEvent,但正如它所说的“此功能不可靠,不应使用。”其文档http://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx 中的注释提供了一些关于为什么不能使用事件对象可靠地实现脉冲式语义的见解。 (基本上,内核有时会暂时将等待事件的线程从其等待列表中移除,因此线程总是有可能错过事件的“脉冲”。无论您使用PulseEvent 还是尝试这样做都是如此自己设置和重置事件。)

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

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

因此您可能需要考虑其他一些同步原语。

【讨论】:

  • 我在我的实际代码中使用 Monitor.PulseAll() 顺便说一句。 ManualResetEventSlim.Set() 的文档说:“将事件的状态设置为已发出信号,这允许 一个或多个线程等待事件继续”从表面上看,是错误的。它应该说零个或多个
  • 在 MSDN 文档中使用 'any' 可以解释为区分 MRE 和 ARE。如果 40 个线程在 MRE 上排队,则表明它应该使所有 40 个线程都准备好。如果没有,它就坏了。
  • 我猜答案是“它不起作用,因为它不是按这种方式设计的,而且 ManualResetEvent.Set() 的 Microsoft 文档不正确且不完整”。
  • 是的,看起来就是这样。谢谢 **** 我不经常使用 MRE!
  • @MatthewWatson 您引文中的关键词是allows - 它并没有说它实际上会导致 任何线程继续进行。 (肯定有这样的情况,文档是“不完整的”(或者,充其量是无助的模糊),因为它们确实应该让它更清楚,但我没有发现任何肯定错误的东西。你引用的措辞严格来说是正确的因为,结合Reset 的文档,您看到的行为实际上并不与文档相矛盾。)“允许”让他们摆脱困境。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-08
  • 1970-01-01
  • 1970-01-01
  • 2016-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多