【问题标题】:ManualResetEvent.WaitOne() doesn't return if Reset() is called immediately after Set()如果在 Set() 之后立即调用 Reset(),ManualResetEvent.WaitOne() 不会返回
【发布时间】:2013-03-08 22:34:04
【问题描述】:

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

这是它目前的工作方式:

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

原因似乎是当多个项目等待处理时,处理第一个项目之后的Set()与处理第二个项目之前的Reset()之间的时间太短,而WaitOne()没有t 似乎认识到该事件已设置。

我对@9​​87654332@ 的理解是被阻塞的线程是guaranteed to receive a signal when Set() is called,但我认为我遗漏了一些重要的东西。

请注意,如果我在调用Set() 之后通过调用Thread.Sleep(0) 来允许上下文切换,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() 并不能保证所有阻塞的线程都会恢复,但是否也不能保证任何等待的线程都会被释放? p>

【问题讨论】:

  • Thread.Sleep(0) 不一定会导致上下文切换——它只会切换到相同优先级的线程。 Thread.Sleep(1) 将切换到不同优先级的线程,如果有等待的话。见bluebytesoftware.com/blog/…
  • 文档还说“如果在所有线程恢复执行之前调用 Reset 方法,则剩余线程再次阻塞”msdn.microsoft.com/en-us/library/ksb7zs2x(v=vs.95).aspx。它还详细说明了自动重置事件不能保证在大致相同的条件下解锁线程 (msdn.microsoft.com/en-us/library/…)
  • 不,它基本上适用于量子。它不能从用户线程中窃取时间来安排另一个线程。它以量子频率分配和调度线程。如果您在该量子范围内切换信号,它可能永远不会“看到”它。
  • 这个以前出现过。好像操作系统状态机坏了。 Set 使线程/s 准备好,但似乎不会将它们从 MRE 等待队列中删除,直到它们真正开始运行。如果在线程仍准备就绪且尚未运行时发生重置,则再次将其设置回“等待”。我不认为这种行为是理智的,但事实就是如此。
  • 恕我直言,在 MRE 上调用 set 应该使所有当前等待的线程准备好并将它们从 MRE 等待队列中删除,以便后续重置无法将它们的状态从准备好更改回等待。我想不出 OS/MRE 的行为方式有任何原因。这很愚蠢。

标签: c# multithreading .net-4.0 manualresetevent


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

    不,这从根本上是错误的代码。当您将 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-02-10
      • 1970-01-01
      • 2021-10-13
      • 2012-01-28
      • 2017-12-11
      • 2014-04-01
      • 2021-01-07
      • 2023-01-03
      • 2013-01-31
      相关资源
      最近更新 更多