开发者

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 开发者_如何学Gosettled 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;
}


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 (() => { ... });


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.


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...


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());


That's how I always convert async function call into sync function call (kudos to somebody on the Internet):

    public static class ThreadingUtils 
    {
         public static TaskScheduler GetScheduler(Dispatcher dispatcher)
         {
             using (var waiter = new ManualResetEvent(false))
             {
                 TaskScheduler scheduler = null;
                 dispatcher.BeginInvoke(new Action(() =>
                 {
                     scheduler = 
                         TaskScheduler.FromCurrentSynchronizationContext();
                     waiter.Set();
                 }));
                 waiter.WaitOne();
                 return scheduler;
             }
         }
    }

Variation:

    if (!waiter.WaitOne(2000))
    { 
        //Timeout connecting to server, log and exit
    }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜