简体   繁体   English

是否可以在处理 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?

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. System.Timers.Timer文档System.Timers.TimerElapsed事件有可能在计时器上调用Dispose后触发。

Is it possible to produce conditions under which this may occur deterministically, or with some statistical likelihood - ie to trap an example of this case?是否有可能产生确定性或具有某种统计可能性的条件 - 即捕获这种情况的一个例子?

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. System.Timers.Timer 的文档说 System.Timers.Timer 的 Elapsed 事件有可能在计时器上调用 Dispose 后触发。

Yes.是的。 Though the particular scenario they are warning about would be very rare.尽管他们警告的特定情况非常罕见。 If the Elapsed event is raised, but cannot get scheduled in the thread pool immediately before the timer is disposed (for example, the thread pool is currently busy with other tasks and so there is a delay before a new thread would be created to service the timer), then once the timer's worker does start, it will see that the timer has been disposed and will refrain from raising the event.如果引发了Elapsed事件,但在处理计时器之前无法立即在线程池中进行调度(例如,线程池当前正忙于其他任务,因此在创建新线程以服务于该计时器之前存在延迟) timer),那么一旦计时器的工作程序启动,它就会看到计时器已被处理,并且不会引发事件。

The only way the event would be raised is if the timer actually starts executing the event-raising logic, performing the disposed check, but before it goes any further, the Windows thread scheduled preempts that thread and schedules the thread that will then immediately dispose the timer.引发事件的唯一方法是,如果计时器实际开始执行事件引发逻辑,执行已处理的检查,但在进一步执行之前,已调度的 Windows 线程会抢占该线程并调度该线程,然后该线程将立即处理计时器。 In this case, raising the event is already in progress, the check for disposed already made, and so once the preempted thread is resumed again, it will proceed even though the timer's already disposed.在这种情况下,引发事件已经在进行中,对已处置的检查已经进行,因此一旦被抢占的线程再次恢复,即使计时器已经处置,它也会继续进行。

As you can imagine, this scenario is very rare.可以想象,这种情况非常罕见。 It can happen, and you should definitely code in defense of it, but it would be very hard to reproduce at will, since we do not have control over the precise sequence of execution within the Timer class itself.它可能发生,您绝对应该编写代码来防御它,但是很难随意重现,因为我们无法控制Timer类本身内的精确执行顺序。

Note: The above is implementation dependent.注意:以上内容取决于实现。 There is nothing in the documentation that promises that exact behavior.文档中没有任何内容承诺这种确切的行为。 While unlikely, it's always possible that the implementation could change such that once the timer has elapsed and the thread pool worker queued to raise the event, no further disposed-check is made.虽然不太可能,但实现总是有可能发生变化,这样一旦计时器过去并且线程池工作线程排队引发事件,就不会进行进一步的处理检查。 It would bad to rely on the rarity of the scenario anyway, but it's especially bad for the reason that the rarity is not even guaranteed.无论如何,依赖场景的稀有性是不好的,但它尤其糟糕,因为甚至不能保证稀有性。

All that said, it is trivial to demonstrate a more realistic problem.综上所述,展示一个更现实的问题是微不足道的。 Because of the way events work in .NET, ie that they can have multiple subscribers, you don't even need some rare thread-scheduling sequence to occur.由于事件在 .NET 中的工作方式,即它们可以有多个订阅者,您甚至不需要一些罕见的线程调度序列来发生。 It is sufficient for one of the handlers for the Elapsed event to dispose the timer. Elapsed事件的处理程序之一处理计时器就足够了。 As long as that handler is subscribed before another, then the other handler will be executed after the timer has been disposed.只要该处理程序在另一个之前订阅,那么另一个处理程序将在计时器被处理后执行。 Here is a code example that demonstrates that:这是一个代码示例,演示了:

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!");

When run, you will see that the "Disposed!"运行时,您会看到"Disposed!" message is displayed before the "Elapsed event raised" .消息显示在"Elapsed event raised"

The bottom line is that you should never write an Elapsed event handler that assumes that code elsewhere that has executed with the intent of stopping the timer, will necessarily be guaranteed to prevent the event handler from executing.最重要的是,你永远不应该编写一个Elapsed事件处理程序,它假设在别处执行的旨在停止计时器的代码必须保证阻止事件处理程序执行。 If the handler has some sort of dependency on state elsewhere in the program, then synchronization and signaling of changes in that dependency must be handled independently of the timer object itself.如果处理程序对程序中其他地方的状态有某种依赖性,则必须独立于计时器对象本身处理该依赖性中更改的同步和信号通知。 You cannot rely on the timer object to successfully prevent execution of a handler.您不能依赖计时器对象来成功阻止处理程序的执行。 Once you've subscribed and started the timer, it is always possible that the event handler might execute and your handler needs to be prepared for that possibility.一旦您订阅并启动了计时器,事件处理程序总是有可能执行的,您的处理程序需要为这种可能性做好准备。


For what it's worth, here's a non-reliable way to demonstrate the out-of-order Elapsed event that uses near-exhaustion of the thread pool to accomplish the effect:值得一提的是,这里有一种不可靠的方式来演示使用线程池接近耗尽来实现效果的无序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!");

When I run this code, more often than not the Elapsed handler is in fact invoked and, when it does, that happens after the timer is disposed.当我运行此代码时,实际上调用了Elapsed处理程序的情况通常会发生,并且当它调用时,会在处理计时器之后发生。

Interestingly, I find this only happens when I almost exhaust the thread pool.有趣的是,我发现这只会在我几乎耗尽线程池时发生。 Ie there is still a thread waiting in the pool ready for the timer object, but the other threads are also kept busy.即仍然有一个线程在池中等待计时器对象,但其他线程也保持忙碌。 If I don't queue any other workers, or if I completely exhaust the thread pool, then by the time the timer gets to the point of its worker running, the timer has been disposed and it suppresses raising the Elapsed event.如果我没有将任何其他工作人员排队,或者如果我完全耗尽了线程池,那么当计时器到达其工作人员运行的点时,计时器已被处置并抑制引发Elapsed事件。

It's clear why completely exhausting the thread pool causes this.很明显为什么完全耗尽线程池会导致这种情况。 Less clear is why it's required to nearly-exhaust the thread pool to see the effect.不太清楚的是为什么需要几乎耗尽线程池才能看到效果。 I believe (but haven't proven) that it's because doing so ensures the CPU cores are kept busy (including the main thread that's also running), allowing the OS thread scheduler to get just enough behind on running the threads that the timer worker gets preempted at just the right time.我相信(但尚未证明)这是因为这样做可确保 CPU 内核保持忙碌(包括也在运行的主线程),从而允许 OS 线程调度程序在运行计时器工作线程时获得足够的延迟恰逢其时。

This isn't 100% reliable, but in my own tests it does demonstrate the behavior more than 50% of the time.这不是 100% 可靠,但在我自己的测试中,它确实在超过 50% 的时间内展示了该行为。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM