在 Timer 被处理后,是否可以生成一个触发 System.Timers.Timer 的 Elapsed 事件的例子?

Is it possible to produce an example of firing Elapsed event of System.Timers.Timer after the Timer has been disposed?

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

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

The documentation for System.Timers.Timer says that it is possible for the Elapsed event of a System.Timers.Timer to fire after the Dispose has been called on the timer.

是的。尽管他们警告的特定情况非常罕见。如果引发了 Elapsed 事件,但无法在计时器被释放之前立即在线程池中进行调度(例如,线程池当前正忙于其他任务,因此在新线程出现之前会有延迟为定时器服务而创建),那么一旦定时器的 worker 启动,它将看到定时器已经被释放并且将避免引发事件。

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

正如你所想象的那样,这种情况是非常罕见的。它可能会发生,你绝对应该编写代码来防御它,但它很难随意重现,因为我们无法控制 Timer class 本身的精确执行顺序.

注意: 以上内容取决于具体实现。文档中没有任何内容可以保证确切的行为。虽然不太可能,但实现总是有可能发生变化,一旦计时器结束并且线程池工作者排队等待引发事件,就不会再进行 disposed-check 了。反正依赖剧情的稀有度就不好了,尤其是稀有度连保证都没有的原因就更糟了。

综上所述,证明一个更现实的问题是微不足道的。由于事件在 .NET 中的工作方式,即它们可以有多个订阅者,您甚至不需要一些罕见的 thread-scheduling 序列发生。 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 事件处理程序,该事件处理程序假设在其他地方执行的代码旨在停止计时器,必然会保证阻止事件处理程序执行。如果处理程序对程序中其他地方的状态有某种依赖性,则该依赖性 中的更改的同步和信号必须 独立于计时器对象本身进行处理。您不能依赖计时器对象来成功阻止处理程序的执行。订阅并启动计时器后,事件处理程序始终有可能执行,您的处理程序需要为这种可能性做好准备。


对于它的价值,这里有一个 non-reliable 方法来演示使用 near-exhaustion 的 out-of-order 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。池中仍有一个线程在等待定时器对象,但其他线程也保持忙碌。如果我不对任何其他工作人员进行排队,或者如果我完全耗尽了线程池,那么当计时器到达其工作人员 运行ning 的点时,计时器已经被处理掉并且它抑制了提高 Elapsed事件。

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

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