Search code examples
.netwpfmultithreadingtaskdispatcher

How can I get a TaskScheduler for a Dispatcher?


I've got an application with multiple Dispatchers (aka GUI threads, aka message pumps) to ensure that a slow, unresponsive portion of the GUI runs without affecting the rest of the application too heavily. I also use Task a lot.

Currently I've got code that conditionally runs an Action on a TaskScheduler or a Dispatcher and then returns a Task either directly or by manually creating one using TaskCompletionSource. However, this split personality design makes dealing with cancellation, exceptions etc. all much more complicated than I'd like. I want to use Tasks everywhere and DispatcherOperations nowhere. To do that I need to schedule tasks on dispatchers - but how?

How can I get a TaskScheduler for any given Dispatcher?

Edit: After the discussion below, I settled on the following implementation:

public static Task<TaskScheduler> GetScheduler(Dispatcher d) {
    var schedulerResult = new TaskCompletionSource<TaskScheduler>();
    d.BeginInvoke(() => 
        schedulerResult.SetResult(
            TaskScheduler.FromCurrentSynchronizationContext()));
    return schedulerResult.Task;
}

Solution

  • Step 1: Create an extension method:

    public static Task<TaskScheduler> ToTaskSchedulerAsync (
        this Dispatcher dispatcher,
        DispatcherPriority priority = DispatcherPriority.Normal) {
    
        var taskCompletionSource = new TaskCompletionSource<TaskScheduler> ();
        var invocation = dispatcher.BeginInvoke (new Action (() =>
            taskCompletionSource.SetResult (
                TaskScheduler.FromCurrentSynchronizationContext ())), priority);
    
        invocation.Aborted += (s, e) =>
            taskCompletionSource.SetCanceled ();
    
        return taskCompletionSource.Task;
    }
    

    Step 2: Use the extension method:

    Old syntax:

    var taskSchedulerAsync = Dispatcher.CurrentDispatcher.ToTaskSchedulerAsync ();
    var taskFactoryAsync = taskSchedulerAsync.ContinueWith<TaskFactory> (_ =>
        new TaskFactory (taskSchedulerAsync.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
    // this is the only blocking statement, not needed once we have await
    var taskFactory = taskFactoryAsync.Result;
    var task = taskFactory.StartNew (() => { ... });
    

    New syntax:

    var taskScheduler = await Dispatcher.CurrentDispatcher.ToTaskSchedulerAsync ();
    var taskFactory = new TaskFactory (taskScheduler);
    var task = taskFactory.StartNew (() => { ... });