TaskScheduler.FromCurrentSynchronizationContext

Stephen Toub - MSFT

The Task abstractions in .NET 4 run on instances of the TaskScheduler class.  Two implementations of TaskScheduler ship as part of the .NET Framework 4.  The first is the default scheduler, which is integrated with the .NET 4 ThreadPool and takes advantage of its work-stealing queues.  The second is the type of TaskScheduler returned from the static method TaskScheduler.FromCurrentSynchronizationContext.

According to MSDN, SynchronizationContext “provides the basic functionality for propagating a synchronization context in various synchronization models.”  What does that really mean?  At its core, SynchronizationContext provides two methods, Send and Post, both of which accept a delegate to be executed.  Send synchronously invokes the delegate, and Post asynchronously invokes the delegate.  That’s it, and the base implementation of SynchronizationContext doesn’t do anything fancier than that:

public virtual void Send(SendOrPostCallback d, object state)
{
    d(state);
}

public virtual void Post(SendOrPostCallback d, object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}

Where things get interesting is when new types are derived from SynchronizationContext, something typically done by a UI framework (though there are other non-UI framework implementations).  For folks familiar with Windows Forms and Windows Presentation Framework development, you’re likely aware that UI controls should only be accessed by the thread that created them, almost always the main UI thread.  Thus, if a thread doing work in the background wants to update something in the UI, it needs to marshal that work back to the GUI thread so that the controls may be accessed safely.  Different UI frameworks expose different ways for accomplishing this marshaling.  For example, in Windows Forms, one uses the Invoke or BeginInvoke method of the target Control (or at least a Control created on the same thread as the target Control).  In WPF, one uses the target thread’s Dispatcher and corresponding Invoke/BeginInvoke  methods.  With every UI framework having its own model for marshaling work to a particular “synchronization context”, it becomes difficult to write code that supports this marshaling concept but which is agnostic to the particular environment that it’s in.  Enter SynchronizationContext.  A new type may be derived from SynchronizationContext such that its Send method synchronously marshals a delegate to the right thread for execution, and Post does the same but asynchronously.  If you look at the implementations of the SynchronizationContexts provided by Windows Forms and WPF, that’s exactly what they do, delegating to the relevant Invoke/BeginInvoke methods from Send and Post to marshal the work correctly.

To make it easy to get at the right SynchronizationContext, a UI framework like Windows Forms will publish an instance of its SynchronizationContext-derived class to SynchronizationContext.Current.  Code can then grab SynchronizationContext.Current and use it to marshal work, without having to know whether it’s being used from Windows Forms or Windows Presentation Foundation or another similar model.

“TaskScheduler.FromCurrentSynchronizationContext” should now make more sense.  This method creates a TaskScheduler that wraps the SynchronizationContext returned from SynchronizationContext.Current.  Thus, this gives you a TaskScheduler that will execute Tasks on the current SynchronizationContext.  Why is that useful?  It means you can create Tasks that are able to access UI controls safely, simply by running them on the right scheduler. 

Let’s say that I wanted to load three images from some data source.  When those images have been loaded, I want to blend them all together, and then I want to display the result into a PictureBox on my UI.  Using Tasks and TaskScheduler.FromCurrentSynchronizationContext, I could write code like the following:

private void Button1_Click(…)
{
    var ui = TaskScheduler.FromCurrentSynchronizationContext();
    var tf = Task.Factory;

    // Load the three images asynchronously
    var imageOne = tf.StartNew(() => LoadFirstImage());
    var imageTwo = tf.StartNew(() => LoadSecondImage());
    var imageThree = tf.StartNew(() => LoadThirdImage());

    // When they’ve been loaded, blend them
    var blendedImage = tf.ContinueWhenAll(
        new [] { imageOne, imageTwo, imageThree }, _ =>
        BlendImages(imageOne.Result, imageTwo.Result, imageThree.Result));

    // When we’re done blending, display the blended image
    blendedImage.ContinueWith(_ =>
    {
        pictureBox1.Image = blendedImage.Result;
    }, ui);
}

This code runs three tasks to load the three input images asynchronously.  When all of those have been loaded, again asynchronously some BlendImages method is used to blend the images, taking the three inputs and returning the blended image.  Finally, once that’s done, another task is used to render the blended image by storing it into a PictureBox on the UI.  Since this modifies a UI control, we need to do it from the UI thread.  Thus, we pass a TaskScheduler to the ContinueWith method; this scheduler targets the UI’s SynchronizationContext, and will cause the Task to execute on the UI thread.

TaskScheduler.FromCurrentSynchronizationContext is provided for convenience and because this is a very common need.  However, due to TaskScheduler’s extensibility, it’s actually possible to implement this behavior yourself, and in doing so you could modify it to suit your own needs however you see fit.

Let’s say you did want to develop a new SynchronizationContextTaskScheduler.  It might look something like this:

public class SynchronizationContextTaskScheduler : TaskScheduler
{
    private ConcurrentQueue<Task> _tasks = new ConcurrentQueue<Task>();
    private SynchronizationContext _context;

    public SynchronizationContextTaskScheduler() :
        this(SynchronizationContext.Current) { }

    public SynchronizationContextTaskScheduler(
        SynchronizationContext context)
    {
        if (context == null) throw new ArgumentNullException(“context”);
        _context = context;
    }

    protected override void QueueTask(Task task)
    {
        // Add the task to the collection
        _tasks.Enqueue(task);

        // Queue up a delegate that will dequeue and execute a task
        _context.Post(delegate
        {
            Task toExecute;
            if (_tasks.TryDequeue(out toExecute)) TryExecuteTask(toExecute);
        }, null);
    }

    protected override bool TryExecuteTaskInline(
        Task task, bool taskWasPreviouslyQueued)
    {
        return SynchronizationContext.Current == _context && 
                  TryExecuteTask(task);
    }

    public override int MaximumConcurrencyLevel { get { return 1; } }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return _tasks.ToArray();
    }
}

Not a lot of code for a fairly powerful thing. 

  • The constructors simply accept the target SynchronizationContext and store it, also initializing a thread-safe queue that will store the tasks to be executed. 
  • The QueueTask method is called whenever the system is providing a Task for this scheduler to execute: this scheduler handles it by storing that Task into a queue, and then Post’ing to the SynchronizationContext a delegate that will pull the next Task from the queue and execute it. 
  • The TryExecuteTaskInline is invoked any time the system wants to run a Task inline on the current thread (either from a call to RunSynchronously or from a Wait attempt): we need to make sure that the call is coming from the same SynchronizationContext as the target, otherwise we may end up running in the wrong context.
  • We only intended to support SynchronizationContexts that represent a single thread of execution (that’s most common), so we return 1 from MaximumConcurrencyLevel.
  • And we want the debugger to be able to display tasks scheduled to this scheduler, so we override GetScheduledTasks to return an array of the tasks queued. (This is why we need to explicitly store the queue of tasks; otherwise, we could have simply relied on lambda closures to capture each task to be executed.)

0 comments

Discussion is closed.

Feedback usabilla icon