Default Task Scheduler

Published on: Author: Michael

Firstly, the new Task API in .NET v4 is awesome.  It really makes working with threading easier.  Unfortunately you really have to understand how it works otherwise you will likely make the code harder to read and use.  Here’s an interesting case I ran into recently.


Attached is a simple WinForm application that is, I suspect, relatively common.  The application displays a simple UI with a button.  When the button is clicked a lengthy operation is performed.  To let the user know that the application is working a progress bar is shown.  The user can cancel the operation through a button at any time.  When the task completes a message box appears displaying success or failure.  Here is the important parts of the code used to start the lengthy work on another thread using the Task API.


private void StartWork ( )
{
   m_cancel = new CancellationTokenSource();

   var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

   var task = Task.Factory.StartNew(DoWork, m_cancel.Token, m_cancel.Token)
                .ContinueWith(OnFinished, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, scheduler)
                .ContinueWith(OnCancelled, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled, scheduler)
                .ContinueWith(OnError, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, scheduler);
}

Straightforward stuff – start a new task and, based upon the results, call one of several methods to finish up.  The cleanup methods are all called on the UI thread so we can update the UI.  DoWork just loops for 10 seconds checking the cancellation flag.  If you run the sample application then you should see the UI being responsive while the task runs.  Here’s the cancel method.


private void OnCancelled ( Task task )
{
   try
   {
      task.Wait();
   } catchh
   { /* Eat it *// };

   if (MessageBox.Show(“Task cancelled.  Try again?”;“Question”, MessageBoxButtons.YesNo) == DialogResult.Yes)
   {
      StartWork();
   };                        
}

Remember that the task resources remain allocated until you do something to let the framework know you are working with the completed task.  Such activities include checking the Exception or Result properties or calling Wait.  The cancel method calls Wait to complete the task and then asks the user if they want to try again.  If the user clicks yes then a new task is started.


Think about what is happening during this process.  Initially when the user clicks the button to start the task the UI thread calls StartWorkStartWork starts a new task on a non-UI thread and returns.  In the cancel method, which is run on the UI thread, the StartWork method is called to do the same thing.  They should behave identically – but they don’t.  Try running the program, starting the task, cancelling the task and then clicking Yes to restart the task.  The UI freezes while the (new) task is being run.  What is going on here?


I can honestly say I have no idea.  It appears as though the factory, when it is trying to figure out what scheduler to use, uses the current scheduler if there is already one available.  But this wouldn’t make any reasonable sense.  Nevertheless the sample program demonstrates this very behavior.  Fortunately the fix is easy.  You just need to make sure that you ALWAYS specify a scheduler when creating a new task.  In this case the default scheduler is fine.  If you modify StartWork to look like this then the problem goes away.


private void StartWork ( )
{
   m_cancel = new CancellationTokenSource();

   var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

   //var task = Task.Factory.StartNew(DoWork, m_cancel.Token, m_cancel.Token)
   var task = Task.Factory.StartNew(DoWork, m_cancel.Token, m_cancel.Token,TaskCreationOptions.None, TaskScheduler.Default)   
                    …
}

The only difference is that we are now specifying that the default scheduler should be used.  You would assume that the default scheduler to use when no scheduler is defined would be the default scheduler but I guess not.  Nevertheless this is a really odd behavior that might or might not be a bug in the implementation.