A reimplementation of .Net's System.Threading.Timer class that solves the unreliable recall problem by allowing callers to associate a version with the timer when changing it.
If you've ever used the Timer
class, you might have noticed that your callbacks can still occur immediately after changing
the timer - you call the Change()
method to postpone the timer, but sometimes the callback still runs. Why?
The reason is that timers have an inherent race condition between scheduling timers and changing timers.
Consider the following sequence of events in a normal timer scenario:
- You schedule a timer to fire 1000 ms in the future.
- At millisecond 1000, two things happen simultaneously: you call
Change()
to try to postpone the timer; and the timer's scheduling logic runs to decide if the callback should be executed. - The Change() code and the scheduling code fight over the timer's internal lock.
- The scheduling code wins the race, and schedules the timer's callback on a background thread.
- The Change() code runs and postpones the timer.
- The callback executes immediately, even though the timer was just postponed.
If the callback's implementation has no mechanism to distinguish between stale and real timeouts, then spurious callbacks may occur.
The VersionedTimer class solves this problem by providing a mechanism to tag the timer with a version - every time the timer is changed, the caller can provide a version field with the new timeouts. Since the timer uses the same lock to process change requests and to schedule callbacks, it's possible to determine if a callback is stale. When the timer's scheduling logic decides to run the timer, it also copies the timer's current version before releasing the lock, to provide to the callback. Since it does so under the same lock it uses for processing Change requests, two things can happen:
- Either the Change() call wins the race, and the timer is successfully rescheduled with an updated version, or:
- The scheduling logic wins the race, reads the old version and schedules the callback with the old version.
Either the timer doesn't run, or it runs with a version that indentifies it as being stale, allowing the callback to filter out the invocation.
Using the timer is fairly simple:
public class TimerExample {
private VersionedTimer<string> timer;
private long version;
private object syncLock;
public TimerExample() {
this.syncLock = new object();
this.version = 0;
this.timer = new VersionedTimer<string>( "Timer", TimerCallback );
}
public void RescheduleTimer() {
lock( this.syncLock ) {
this.version++;
// Postpone the timer so that it starts in 555 ms.
this.timer.Change( 555, 42, this.version );
}
}
private void TimerCallback( string state, long callbackVersion ) {
lock( this.syncLock ) {
if( callbackVersion < this.version ) {
// Stale timer callback. Ignore it.
return;
}
ProcessTimerCallback();
}
}
}
This project is licensed under the BSD 2-clause license.