System.Timers.Timer的文档说 System.Timers.Timer 的Elapsed
事件可能Dispose
在计时器上被调用后触发。
是否有可能产生确定性或具有某种统计可能性的条件 - 即捕获这种情况的示例?
System.Timers.Timer的文档说 System.Timers.Timer 的Elapsed
事件可能Dispose
在计时器上被调用后触发。
是否有可能产生确定性或具有某种统计可能性的条件 - 即捕获这种情况的示例?
System.Timers.Timer 的文档说,在计时器上调用 Dispose 后,System.Timers.Timer 的 Elapsed 事件可能会触发。
是的。尽管他们警告的特定情况非常罕见。如果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
实际上通常会调用处理程序,并且当它调用时,这会在定时器被释放之后发生。
有趣的是,我发现这只发生在我几乎耗尽线程池时。即在池中仍有一个线程等待定时器对象,但其他线程也保持忙碌。如果我没有将任何其他工作人员排队,或者如果我完全耗尽了线程池,那么当计时器到达其工作人员运行的点时,计时器已被释放并抑制引发Elapsed
事件。
很清楚为什么完全耗尽线程池会导致这种情况。不太清楚的是为什么需要几乎耗尽线程池才能看到效果。我相信(但尚未证明)这是因为这样做可以确保 CPU 内核保持忙碌(包括也在运行的主线程),从而允许操作系统线程调度程序在运行计时器工作者获得的线程时获得足够的支持在正确的时间抢占先机。
这不是 100% 可靠的,但在我自己的测试中,它确实有超过 50% 的时间证明了这种行为。