【问题标题】:Is it possible to produce an example of firing Elapsed event of System.Timers.Timer after the Timer has been disposed?是否可以在 Timer 被处理后生成触发 System.Timers.Timer 的 Elapsed 事件的示例?
【发布时间】:2023-04-07 01:10:01
【问题描述】:

documentation for System.Timers.Timer 表示 System.Timers.Timer 的 Elapsed 事件有可能在定时器上调用 Dispose 后触发。

是否有可能产生确定性或具有某种统计可能性的条件 - 即捕获这种情况的示例?

【问题讨论】:

    标签: c# .net timer system.timers.timer


    【解决方案1】:

    System.Timers.Timer 的文档说 System.Timers.Timer 的 Elapsed 事件可能会在对计时器调用 Dispose 后触发。

    是的。尽管他们警告的特定情况非常罕见。如果引发了Elapsed 事件,但无法在定时器释放之前立即在线程池中调度(例如,线程池当前正忙于其他任务,因此在创建新线程之前存在延迟)服务计时器),然后一旦计时器的工作人员启动,它将看到计时器已被释放,并将避免引发事件。

    引发事件的唯一方法是,如果计时器实际上开始执行事件引发逻辑,执行已处置的检查,但在进一步进行之前,Windows 线程调度会抢占该线程并调度随后将执行的线程立即处置计时器。在这种情况下,引发事件已经在进行中,已经进行了对已释放的检查,因此一旦被抢占的线程再次恢复,即使计时器已经释放,它也会继续进行。

    您可以想象,这种情况非常罕见。它可能会发生,你绝对应该编写代码来保护它,但它很难随意重现,因为我们无法控制 Timer 类本身的精确执行顺序。

    注意:以上内容取决于实现。文档中没有任何内容可以保证确切的行为。虽然不太可能,但实现总是有可能发生变化,这样一旦计时器结束并且线程池工作人员排队引发事件,就不会进行进一步的处置检查。无论如何都依赖场景的稀有性是不好的,但由于甚至无法保证稀有性,这尤其糟糕。

    话虽如此,证明一个更现实的问题是微不足道的。由于事件在 .NET 中的工作方式,即它们可以有多个订阅者,您甚至不需要发生一些罕见的线程调度序列。 Elapsed 事件的处理程序之一足以处置计时器。只要该处理程序在另一个处理程序之前被订阅,那么另一个处理程序将在计时器被释放后执行。这是一个演示代码示例:

    Timer timer = new Timer(1000);
    SemaphoreSlim semaphore = new SemaphoreSlim(0);
    
    timer.Elapsed += (sender, e) =>
    {
        WriteLine("Disposing...");
        ((Timer)sender).Dispose();
    };
    
    timer.Disposed += (sender, e) =>
    {
        WriteLine("Disposed!");
    };
    
    timer.Elapsed += (sender, e) =>
    {
        WriteLine("Elapsed event raised");
        semaphore.Release();
    };
    
    timer.Start();
    WriteLine("Started...");
    semaphore.Wait();
    WriteLine("Done!");
    

    运行时,你会看到"Disposed!"消息显示在"Elapsed event raised"之前。

    最重要的是,您永远不应该编写一个Elapsed 事件处理程序,它假定在其他地方执行的旨在停止计时器的代码必然会保证阻止事件处理程序执行。如果处理程序对程序中其他地方的状态有某种依赖关系,则必须独立于计时器对象本身处理该依赖关系中的同步和更改信号。您不能依赖计时器对象来成功阻止处理程序的执行。一旦您订阅并启动了计时器,事件处理程序总是有可能执行,您的处理程序需要为这种可能性做好准备。


    不管怎样,这里有一个不可靠的方法来演示使用线程池接近耗尽来实现效果的无序Elapsed 事件:

    int regular, iocp;
    int started = 0;
    
    void Started()
    {
        started++;
        WriteLine($"started: {started}");
    }
    
    ThreadPool.GetMinThreads(out regular, out iocp);
    
    WriteLine($"regular: {regular}, iocp: {iocp}");
    
    regular -= 1;
    
    CountdownEvent countdown = new CountdownEvent(regular);
    
    while (regular-- > 0)
    {
        ThreadPool.QueueUserWorkItem(_ => { Started(); Thread.Sleep(1000); countdown.Signal(); });
    }
    
    Timer timer = new Timer(100);
    
    timer.Elapsed += (sender, e) => WriteLine("Elapsed event raised");
    WriteLine("Starting timer...");
    timer.Start();
    Thread.Sleep(100);
    WriteLine("Disposing timer...");
    timer.Dispose();
    
    WriteLine("Workers queued...waiting");
    countdown.Wait();
    WriteLine("Workers done!");
    

    当我运行这段代码时,Elapsed 处理程序实际上会被调用,而当它调用时,这会在定时器被释放后发生。

    有趣的是,我发现只有当我几乎耗尽线程池时才会发生这种情况。 IE。池中仍有一个线程等待计时器对象,但其他线程也保持忙碌。如果我没有对任何其他工作人员进行排队,或者如果我完全耗尽了线程池,那么当计时器到达其工作人员运行的点时,计时器已被释放,它会抑制引发Elapsed 事件。

    很清楚为什么线程池完全耗尽会导致这种情况。不太清楚的是为什么需要几乎耗尽线程池才能看到效果。我相信(但尚未证明)这是因为这样做可以确保 CPU 内核保持忙碌(包括也在运行的主线程),从而允许操作系统线程调度程序在运行计时器工作者获得的线程时获得足够的支持在正确的时间抢占。

    这不是 100% 可靠的,但在我自己的测试中,它确实有超过 50% 的时间证明了这种行为。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-26
      相关资源
      最近更新 更多