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

Sim*_*zie 4 c# multithreading .net-4.0 manualresetevent

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

以下是它目前的工作原理:

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

原因似乎是当多个项目等待处理时,处理Set()第一个项目之后和处理Reset()第二个项目之间的时间太短,并且WaitOne()似乎没有认识到事件已被设置.

我的理解WaitOne()是被阻塞的线程Set()被调用时保证会收到一个信号,但我想我错过了一些重要的东西.

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出上面的代码


问题

虽然我理解Set()紧随其后的调用Reset()并不能保证所有被阻塞的线程都会恢复,但是也不保证会释放任何等待的线程吗?

Han*_*ant 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
    }
}
Run Code Online (Sandbox Code Playgroud)


Ste*_*ary 6

你不能像这样"脉动"一个OS事件.

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

顺便说一句,这确实意味着PulseEventWin32 API 完全被破坏了.

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

在您的情况下,我会考虑切换到工作过程AutoResetEvent重复Set的一个,并且每次Wait完成时由看门狗进程(自动)重置.并且你可能想要通过每分钟检查一次来"驯服"看门狗.