This project has moved and is read-only. For the latest updates, please go here.

[Info] Asynchronous Programming: Throttling to improve responsiveness

Dec 28, 2014 at 11:05 AM
Edited May 14, 2016 at 8:27 PM

This article has been moved to GitHub!


Throttling helps to improve the responsiveness of an application by delaying a method call and skipping all additional calls of this method during the delay.

Example
The Waf Music Manager uses the WPF Slider Control to represent the current position of the playing music file. The Slider provides the ValueChanged event to listen for the slider position changes. But this event is raised very often when a user moves the slider. However, repositioning the current playing music file via the MediaPlayer should not be done multiple times within some milliseconds. Throttling is used so that at maximum every 100 milliseconds the position of the MediaPlayer is updated.
private readonly ThrottledAction throttledSliderValueChangedAction;
private double lastUserSliderValue;

public PlayerView()
{
    throttledSliderValueChangedAction = new ThrottledAction(ThrottledSliderValueChangedAction, TimeSpan.FromMilliseconds(100));
}

private void PositionSliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    lastUserSliderValue = e.NewValue;
    throttledSliderValueChangedAction.InvokeAccumulated();
}

private void ThrottledSliderValueChangedAction()
{
    mediaPlayer.Position = TimeSpan.FromSeconds(lastUserSliderValue);
}
The ThrottledAction class is a simple implementation of throttling. It requires the method which will be called throttled and the delay time. The InvokeAccumulated method is used to call the specified method delayed. Multiple calls of the InvokeAccumulated method within the delay time results in just one call of the throttled method.
public class ThrottledAction
{
    private readonly TaskScheduler taskScheduler;
    private readonly object timerLock = new object();
    private readonly Timer timer;
    private readonly Action action;
    private readonly TimeSpan dueTime;
    private volatile bool isRunning;

    public ThrottledAction(Action action) : this(action, TimeSpan.FromMilliseconds(10))
    {
    }

    public ThrottledAction(Action action, TimeSpan dueTime)
    {
        this.taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        this.timer = new Timer(TimerCallback);
        this.action = action;
        this.dueTime = dueTime;
    }

    public bool IsRunning { get { return isRunning; } }

    public void InvokeAccumulated()
    {
        lock (timerLock)
        {
            if (!isRunning)
            {
                isRunning = true;
                timer.Change(dueTime, Timeout.InfiniteTimeSpan);
            }
        }
    }

    private void TimerCallback(object state)
    {
        lock (timerLock)
        {
            isRunning = false;
        }
            
        Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, taskScheduler);
    }
}
Implementation notes
  • The method call will run synchronized when a synchronization context exists. Therefore, the method call will run in the Dispatcher UI thread when the ThrottledAction is used in a WPF application.
  • It is not possible to pass method arguments within the InvokeAccumulated method. This design decision was made because the passed argument values might be outdated at the time when the throttled method will be called.
  • The implementation does not implement the IDisposable interface although the used Timer does. I do not like to implement the IDisposable pattern in all classes that are using the ThrottledAction. As a result the timer will be disposed when the garbage collector collects the ThrottledAction with its Timer instance.
Other implementations
  • The Reactive Extensions (Rx) library provides the Observable.Throttle Method
  • The WPF Dispatcher knows different priorities. This concept results also in a throttled behavior. Example: Not every raise of the PropertyChanged event does immediately update the Binding Targets. Binding has its own Dispatcher priority “DataBind” so that updating the target is done delayed.