如果在Set()之后立即调用Reset(),则ManualResetEvent.WaitOne()不会返回

时间:2013-03-20 00:45:57

标签: c# multithreading .net-4.0 manualresetevent

我在生产服务中遇到问题,该服务包含一个“看门狗”计时器,用于检查主处理作业是否已冻结(这与COM互操作问题有关,遗憾的是,在测试中无法再现)。 / p>

以下是目前的工作原理:

  • 在处理过程中,主线程重置ManualResetEvent,处理单个项目(这不应该花费很长时间),然后设置事件。然后它继续处理任何剩余的项目。
  • 每隔5分钟,看门狗就此事件呼叫WaitOne(TimeSpan.FromMinutes(5))。如果结果为false,则重新启动服务。
  • 有时,在正常操作期间,即使处理时间不到5分钟,该监视程序也会重新启动该服务。

原因似乎是当多个项目等待处理时,处理第一个项目之后的Set()与处理第二个项目之前的Reset()之间的时间太短,{{ {1}}似乎无法识别事件已设置。

我对WaitOne()的理解是被阻止的帖子是guaranteed to receive a signal when Set() is called,但我认为我遗漏了一些重要内容。

请注意,如果我在调用WaitOne()后调用Thread.Sleep(0)允许上下文切换,则Set()永远不会失败。

下面是一个与我的生产代码产生相同行为的示例。 WaitOne()有时会等待5秒并失败,即使每隔800毫秒调用WaitOne()也会失败。

Set()

output of the above code


问题

虽然我理解紧跟private static ManualResetEvent _handle; private static void Main(string[] args) { _handle = new ManualResetEvent(true); ((Action) PeriodicWait).BeginInvoke(null, null); ((Action) PeriodicSignal).BeginInvoke(null, null); Console.ReadLine(); } private static void PeriodicWait() { Stopwatch stopwatch = new Stopwatch(); while (true) { stopwatch.Restart(); bool result = _handle.WaitOne(5000, false); stopwatch.Stop(); Console.WriteLine("After WaitOne: {0}. Waited for {1}ms", result ? "success" : "failure", stopwatch.ElapsedMilliseconds); SpinWait.SpinUntil(() => false, 1000); } } private static void PeriodicSignal() { while (true) { _handle.Reset(); Console.WriteLine("After Reset"); SpinWait.SpinUntil(() => false, 800); _handle.Set(); // Uncommenting either of the lines below prevents the problem //Console.WriteLine("After Set"); //Thread.Sleep(0); } } 之后调用Set()并不能保证所有被阻止的线程都会恢复,但是也无法保证任何等待线程将会释放?

2 个答案:

答案 0 :(得分:11)

不,这是从根本上打破的代码。当您将MRE设置为如此短的时间时,WaitOne()将只有合理的赔率。 Windows赞成释放在事件中被阻止的线程。但是当线程没有等待时,这将彻底失败。或者调度程序选择另一个线程,一个以更高优先级运行并且也被解除阻塞的线程。例如,可以是内核线程。 MRE没有保留已经发出信号而尚未等待的“记忆”。

Sleep(0)或Sleep(1)都不足以保证等待即将完成,调度程序绕过等待线程的频率没有合理的上限。虽然你需要在超过10秒的时间内关闭程序;)

你需要以不同的方式做到这一点。一种简单的方法是依靠worker最终设置事件。所以在开始等待之前重置它:

private static void PeriodicWait() {
    Stopwatch stopwatch = new Stopwatch();

    while (true) {
        stopwatch.Restart();
        _handle.Reset();
        bool result = _handle.WaitOne(5000);
        stopwatch.Stop();
        Console.WriteLine("After WaitOne: {0}. Waited for {1}ms", result ? "success" : "failure",
                            stopwatch.ElapsedMilliseconds);
    }
}

private static void PeriodicSignal() {
    while (true) {
        _handle.Set();
        Thread.Sleep(800);   // Simulate work
    }
}

答案 1 :(得分:6)

你不能“脉动”这样的OS事件。

在其他问题中,有一个事实是,在OS句柄上执行阻塞等待的任何OS线程都可以被内核模式APC暂时中断;当APC完成时,线程重新开始等待。如果在中断期间发生脉冲,则线程看不到它。这只是如何错过“脉冲”的一个例子(详见Concurrent Programming on Windows,第231页)。

顺便说一句,这确实意味着PulseEvent Win32 API是completely broken

在具有托管线程的.NET环境中,更有可能错过脉冲。垃圾收集等

在您的情况下,我会考虑切换到工作流程重复AutoResetEvent的{​​{1}},并在每次Set完成时由监督程序进程(自动)重置。而且你可能想要通过每分钟检查一次来“驯服”看门狗。