4

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

以下是它目前的工作方式:

  • 在处理期间,主线程重置 a ManualResetEvent,处理单个项目(这不应该花费很长时间),然后设置事件。然后它继续处理任何剩余的项目。
  • 每 5 分钟,看门狗就会调用WaitOne(TimeSpan.FromMinutes(5))此事件。如果结果为假,则重新启动服务。
  • 有时,在正常操作期间,该看门狗会重新启动服务,即使处理时间不到 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);
    }
}

上述代码的输出


问题

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

4

2 回答 2

11

不,这从根本上是错误的代码。当您将 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
    }
}
于 2013-03-20T01:30:47.913 回答
6

您不能像这样“脉冲”操作系统事件。

除其他问题外,任何在 OS 句柄上执行阻塞等待的 OS 线程都可能被内核模式 APC 临时中断。当 APC 完成时,线程继续等待。如果脉冲发生在中断期间,线程看不到它。这只是如何遗漏“脉冲”的一个示例(在 Windows 上的并发编程中进行了详细描述,第 231 页)。

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

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

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

于 2013-03-20T01:38:07.313 回答