简体   繁体   中英

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.

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.

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.

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. 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.

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. It is sufficient for one of the handlers for the Elapsed event to dispose the timer. 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!" message is displayed before the "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. 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:

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.

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.

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.

This isn't 100% reliable, but in my own tests it does demonstrate the behavior more than 50% of the time.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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