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

当前的工作方式如下:

  • 在处理过程中,主线程将重置 ManualResetEvent ,处理单个项目(此过程不应花费很长时间),然后设置事件。然后,它将继续处理所有剩余项目。
  • 每5分钟,看门狗在此事件中调用WaitOne(TimeSpan.FromMinutes(5))。如果结果为假,则服务将重新启动。
  • 有时,在正常操作期间,即使处理时间不超过5分钟,此看门狗也会重新启动服务。

  • 原因似乎是当有多个项目等待处理时,处理第一个项目之后的Set()和处理第二个项目之前的Reset()之间的时间太短,并且WaitOne()似乎无法识别出该事件已经发生。放。

    我对WaitOne()的理解是被阻止的线程是guaranteed to receive a signal when Set() is called,但是我想我缺少了一些重要的东西。

    请注意,如果在调用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);
        }
    }
    



    问题

    虽然我知道紧跟在Set()之后再调用Reset()并不能保证所有被阻塞的线程都将恢复,但是还不能保证将释放任何等待的线程吗?

    最佳答案

    不,这基本上是损坏的代码。当您将 MRE 设置保持如此短的时间时,WaitOne() 完成的可能性只有合理的可能性。 Windows 倾向于释放被事件阻塞的线程。但是当线程不等待时,这将彻底失败。或者调度程序选择另一个线程,该线程以更高的优先级运行并且也被解除阻塞。例如,可能是内核线程。 MRE 不会保留已发出信号但尚未等待的“内存”。

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

    您需要以不同的方式执行此操作。一个简单的方法是依靠工作人员来最终设置事件。所以在你开始等待之前重置它:

    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
        }
    }
    

    10-06 05:47