How can I get a TaskScheduler for a Dispatcher?
Asked Answered
I

6

20

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;
}
Impress answered 16/6, 2011 at 8:9 Comment(0)
A
16

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 (() => { ... });
Akron answered 2/5, 2012 at 16:7 Comment(5)
Nice touch with the Aborted event! The only remaining downside is that the task scheduler is created asynchronously; i.e. that you have to wait to schedule tasks until the dispachter has run the invocation (not a major issue, since you can ContinueWith, but it's a little unhandy)Impress
well, await does effectively the same thing here; namely result in a Task that is executed asynchronously after the Dispatcher has run. In particular, that means that when the execution of the containing method is complete, the task scheduler may not yet be available (i.e. The issue is still the same in .NET 4.5). It's not a critical issue; anyhow it can't be worked around as long as the only way to create the scheduler is on the sync context itself (which is an odd API limitation, but it is what it is...)Impress
What's the thinking behind setting the default priority to Send over Normal?Jackpot
@FactorMystic It looks like George incorporated your feedback. Now we're into 'long tail' territory, maybe delete your comment?Blinker
@FactorMystic: "Send" is the highest priority available. Its name probably comes from "SendMessage"/"PostMessage" pair in Win32 message loop, where "Post" meant "Enqueue the message" and "Send" meant "process the message synchronously", which usually effetively meant "do it right now", what quite matches a notion of "highest priority". Speaking of it, since the "delayed TaskScheduler creation" might be an issue, we'd like to have it "immediatelly", hence highest priority. Still it of course would not solve the problem, just reduce the odds of happening by a bit.Borodino
S
9

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.
Saddler answered 16/6, 2011 at 12:22 Comment(1)
I can only accept one answer, but that's the only reason I'm not accepting this - this is pretty much exactly what I ended up doing. Thanks!Impress
E
4

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

Embryo answered 16/6, 2011 at 8:25 Comment(4)
I think I see where you're going - but FromCurrentSynchronizationContext doesn't take a Dispatcher as parameter, and I've got several. If you make the solution explicit, your answer would be useful to others and I'd accept it ;-)Impress
Hm, I tried to get a bit more explicit, see the edited answer. Also, consider Ian´s proposal to spin own TaskScheduler which might encapsulate the context switching in a more appropriate way...Embryo
That does help, but your example included some extraneous bits concerning Text's and Int32; I edited it to reflect the actual method I ended up using. That's perhaps a little invasive, please do fix the answer to reflect your opinion if I've changed too much (and sorry if that's so...).Impress
Yeah, you've completely re-wrote it :-) My intention was to show how to fetch a sync. context and schedule a continuation task on it. If this basically solves your problem, I'd rather cut'n'paste the snippet into your question.Embryo
L
4

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());
Leader answered 13/11, 2014 at 19:3 Comment(2)
I could not have, since .NET 4.5 wasn't around when I wrote that. But yes - you could now! :-)Impress
correct, that occurred to me a few hours after I posted my response.Leader
L
1

Although there is no built-in class dedicated to wrapping a Dispatcher in a TaskScheduler, you're allowed to write one by yourself. Sample implementation that does the job looks like that:

internal class DispatcherTaskScheduler : TaskScheduler
{
    private readonly Dispatcher _dispatcher;
    private readonly SendOrPostCallback _dispatcherCallback;

    public DispatcherTaskScheduler(Dispatcher dispatcher)
    {
        _dispatcher = dispatcher;
        // Callback for dispatcher.
        _dispatcherCallback = (state) =>
        {
            _ = TryExecuteTask((Task)state!);
        };
    }

    public override int MaximumConcurrencyLevel
    {
        // Dispatcher operates on one thread only.
        get => 1;
    }

    protected override IEnumerable<Task> GetScheduledTasks()
    {
        // Whole method is only for debugging purposes. No need to implement it.
        return Array.Empty<Task>();
    }

    protected override void QueueTask(Task task)
    {
        // Schedule the task for execution.
        _ = _dispatcher.BeginInvoke(DispatcherPriority.Normal, _dispatcherCallback, task);
    }

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        // If we are in the right thread then execute the task.
        if (_dispatcher.CheckAccess())
            return TryExecuteTask(task);
        return false;
    }
}
Lilith answered 14/7, 2023 at 9:22 Comment(0)
P
0

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
    }
Plumbic answered 23/8, 2011 at 14:57 Comment(4)
while this will work, why use manual waiting when you're using the TPL already anyhow? The TaskCompletionSource approach lets you block if you need to, doesn't block if you don't, and is shorter to boot.Impress
Correct, I could also call Dispatcher.Invoke(...) which would do exactly the same internally... My point was to show the generic pattern applicable to ANY async call... Also, what about timeout handling?Plumbic
Also, what about returning multiple result sets? And multiple waiting blocks? Btw, I think that's how Microsoft implemented "async" and "await" functionality...Plumbic
No, this isn't the same as using TaskCompletionSource internally; that may not require locking or events depending on what's waiting on the result and depending on the TaskScheduler controlling that waiting. And it almost certainly won't require the heavy OS-level ManualResetEvent - which is a multi-process synchronization tool rather than a merely multi-thread synchronization tool.Impress

© 2022 - 2025 — McMap. All rights reserved.