Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
}
like image 205
Eamon Nerbonne Avatar asked Jun 16 '11 08:06

Eamon Nerbonne


4 Answers

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 (() => { ... });
like image 60
George Tsiokos Avatar answered Nov 07 '22 14:11

George Tsiokos


Unfortunately, there's no built-in way to do this. There is no built-in class dedicated to wrapping a Dispatcher in a TaskScheduler - the closest thing we have is the one that wraps a SynchronizationContext. And the only public API for building a TaskScheduler from a SynchronizationContext is the one Paul Michalik mentions: TaskScheduler.FromCurrentSynchronizationContext - and as you observe, that only works if you're already in the relevent synchronization context (i.e., on the relevant dispatcher's thread).

So you have three choices:

  1. Arrange your code so that the class that needs schedulers for the relevant dispatchers will get a chance to run on those dispatchers' threads at some point, so that you can use TaskScheduler.FromCurrentSynchronizationContext as intended.
  2. Use Dispatcher.BeginInvoke to run some code on the dispatcher thread, and in that code, call TaskScheduler.FromCurrentSynchronizationContext. (In other words, if you can't arrange for 1. to happen naturally, force it to happen.)
  3. Write your own task scheduler.
like image 24
Ian Griffiths Avatar answered Nov 07 '22 13:11

Ian Griffiths


Have a look at TaskScheduler.FromCurrentSynchronizationContext. The Task framework provides a very flexible way to configure the execution of compute bound operations even if there is a specific threading model imposed by the application.

EDIT:

Hm, it is hard to get more explicit from what you have posted. I understand that you´re running sort of multi-view application with separate dispatchers for each view, right? Since all the dispatching boils down to fetching a SynchronizationContext and Post-ing to it you can fetch the right TaskScheduler (the one with the correct SynchronizationContext) at some point where your view(s) got one. A simple way to do that would be to get a TaskScheduler during the configuration of the taks(s):

 // somewhere on GUI thread you wish to invoke
 // a long running operation which returns an Int32 and posts
 // its result in a control accessible via this.Text
 (new Task<Int32>(DoSomeAsyncOperationReturningInt32)
      .ContinueWith(tTask => this.Text = tTask.Result.ToString(),
                    TaskScheduler.FromCurrentSynchronizationContext)).Start();

Not sure if this helps, if your are using Tasks extensively you´ll probably already know that...

like image 4
Paul Michalik Avatar answered Nov 07 '22 13:11

Paul Michalik


You could have written the whole function in one line:

public static Task<TaskScheduler> ToTaskSchedulerAsync(this Dispatcher dispatcher,
                           DispatcherPriority priority = DispatcherPriority.Normal)
{
    return dispatcher.InvokeAsync<TaskScheduler>(() =>
         TaskScheduler.FromCurrentSynchronizationContext(), priority).Task;
}

and those who are content with the default UI thread may find the following enough to get by:

var ts = Application.Current.Dispatcher.Invoke<TaskScheduler>(() => TaskScheduler.FromCurrentSynchronizationContext());
like image 3
SmallBizGuy Avatar answered Nov 07 '22 14:11

SmallBizGuy