Skip to content

A C# timer that uses versioning to track unreliable recall

License

Notifications You must be signed in to change notification settings

antiduh/VersionedTimer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VersionedTimer

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.

Unreliable Recall

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.

Versioning - A solution

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.

An Example

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();
        }
    }
}

License

This project is licensed under the BSD 2-clause license.

About

A C# timer that uses versioning to track unreliable recall

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages