Introduction
This tip shows how you can schedule an action to be run after a quiet period in case you are listening to an event
which is raised frequently.
Background
I had this problem recently when developing a plugin. Our code was tracking changes in an object graph and doing some
calculations based on the new state. The problem was that the event was raised in batches as the changes were cascaded
through the graph. To prevent unnecessary re-calculations, we decided to wait until change propagates through the graph
and all change events are fired. Only after a quiet period, the handler is invoked.
One way to do it is to use a timer and then stop and restart it each time a notification arrives. When looking at
the implementation of System.Timers.Timer
and System.Threading.Timer
(the former uses the latter
internally), I’ve got the impression that a timer is too heavyweight for this task. Also to make timer stop reliably
requires further effort. But the same can be achieved simply by using a lock, a thread and Monitor
class.
The Code
We create a class that will act as a timer with the ability to restart the countdown when notification arrives.
Let’s assume the following declarations.
private int _timeout;
private object _lock = new Object();
private bool _isRunning = false;
public event EventHandler TimeoutElapsed;
On each change, we will call a method that will start or restart the timer respectively.
public void Ping()
{
lock (_lock)
{
if (_isRunning)
{
Monitor.Pulse(_lock);
}
else
{
_isRunning = true;
Task.Factory.StartNew(WaitForTimeout);
}
}
}
When timer is already started, pulse the lock to reset the timer. Otherwise, start a background thread that will wait
until timeout expires.
private void WaitForTimeout()
{
lock (_lock)
{
while (Monitor.Wait(_lock, _timeout)) ;
_isRunning = false;
}
RaiseTimeoutElapsed();
}
The background thread waits on the lock for a specified time. If the lock is pulsed and the thread awakes before timeout
has expired, it simply goes to sleep again. Otherwise if the thread is not awakened prematurely and timeout expires, it raises an event.
private void RaiseTimeoutElapsed()
{
var handler = TimeoutElapsed;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
Points of Interest
This is not reliable in the sense that there is no guarantee that the gap between events in the same logical batch
will not exceed the given timeout. It’s up to you to choose proper timeout value.
If you plan to update UI in the callback, don't forget that it is fired on background thread. You need to marshal
the update to the main thread.
If there is an exception thrown in the handler, it will be confined to the background thread and eventually end up in
TaskScheduler.UnobservedTaskException
. You may want to add some exception handling code to
RaiseTimeoutElapsed()
method as well.