How can I get a TaskScheduler for a Dispatcher?
I've got an application with multiple Dispatcher
s (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 Task
s everywhere and DispatcherOperation
s 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:
- 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. - Use
Dispatcher.BeginInvoke
to run some code on the dispatcher thread, and in that code, callTaskScheduler.FromCurrentSynchronizationContext
. (In other words, if you can't arrange for 1. to happen naturally, force it to happen.) - 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
}
精彩评论