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

问题描述 投票:0回答:2

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

目前的运作方式如下:

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

原因似乎是当多个项目等待处理时,处理第一个项目后的

Set()
和处理第二个项目之前的
Reset()
之间的时间太短,并且不出现
WaitOne()
确认事件已设置。

我对

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

请注意,如果我在调用

Thread.Sleep(0)
后通过调用
Set()
来允许上下文切换,则
WaitOne()
永远不会失败。

下面包含一个示例,它产生与我的生产代码相同的行为。

WaitOne()
有时会等待 5 秒并失败,即使每 800 毫秒调用一次
Set()

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

output of the above code


问题

虽然我知道紧随其后调用

Set()
并不能保证所有被阻塞的线程都会恢复,但是否也不能保证
任何等待线程都会被释放?

不,这从根本上来说是损坏的代码。当您将 MRE 设置如此短的时间时,WaitOne() 完成的可能性只有“合理”。 Windows 倾向于释放因事件而阻塞的线程。但当线程不等待时,这将彻底失败。或者调度程序选择另一个线程,该线程以更高的优先级运行并且也被解除阻塞。例如,可以是内核线程。 MRE 不会保留已发出信号但尚未等待的“记忆”。
c# multithreading .net-4.0 manualresetevent
2个回答
13
投票
Sleep(0) 或 Sleep(1) 都不足以

保证等待将完成,对于调度程序绕过等待线程的频率没有合理的上限。虽然当程序运行时间超过 10 秒时你可能应该关闭它;)

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

Reset()

您无法像这样“脉冲”操作系统事件。
除其他问题外,任何在操作系统句柄上执行阻塞等待的操作系统线程都可能被内核模式 APC 暂时中断;当 APC 完成时,线程恢复等待。如果脉冲发生在该中断期间,则线程看不到它。这只是如何错过“脉冲”的一个示例(详细描述见

7
投票
,第 231 页)。

顺便说一句,这确实意味着

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 } } Win32 API 完全损坏了

在具有托管线程的 .NET 环境中,丢失脉冲的可能性更大。垃圾收集等
在你的情况下,我会考虑切换到PulseEvent

,它由工作进程重复

AutoResetEvent

,并在每次

Set

完成时由看门狗进程(自动)重置。您可能希望通过只让看门狗每分钟左右检查一次来“驯服”看门狗。
© www.soinside.com 2019 - 2024. All rights reserved.