20

I've read this question from Noseratio which shows a behaviour where TaskScheduler.Current is not the same after an awaitable has finished its operation.

The answer states that :

If there is no actual task being executed, then TaskScheduler.Current is the same as TaskScheduler.Default

Which is true . I already saw it here :

  • TaskScheduler.Default
    • Returns an instance of the ThreadPoolTaskScheduler
  • TaskScheduler.Current
    • If called from within an executing task will return the TaskScheduler of the currently executing task
    • If called from any other place will return TaskScheduler.Default

But then I thought , If so , Let's do create an actual Task (and not just Task.Yield()) and test it :

async void button1_Click_1(object sender, EventArgs e)
{
    var ts = TaskScheduler.FromCurrentSynchronizationContext();
    await Task.Factory.StartNew(async () =>
    {
        MessageBox.Show((TaskScheduler.Current == ts).ToString()); //True

           await new WebClient().DownloadStringTaskAsync("http://www.google.com");

        MessageBox.Show((TaskScheduler.Current == ts).ToString());//False

    }, CancellationToken.None, TaskCreationOptions.None,ts).Unwrap();
}

First Messagebox is "True" , second is "False"

Question:

As you can see , I did created an actual task.

I can understand why the first MessageBox yield True. Thats becuase of the :

If called from within an executing task will return the TaskScheduler of the currently executing task

And that task does have ts which is the sent TaskScheduler.FromCurrentSynchronizationContext()

But why the context is not preserved at the second MessageBox ? To me , It wasn't clear from Stephan's answer.

Additional information :

If I write instead (of the second messagebox ) :

MessageBox.Show((TaskScheduler.Current == TaskScheduler.Default).ToString());

It does yield true . But why ?

Community
  • 1
  • 1
Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • Excellent question, but what if `task scheduler != synchronization context`? – AgentFire Aug 26 '15 at 14:13
  • A good case for the hard rule: never assume `TaskScheduler.Current` is what you think it is :) It's only guaranteed to be what you passed to `Factory.StartNew` for the scope of the task action lambda (which is `Func` in your case and returns when it hits the first `await`). Any other behavior should be treated as implementation details. – noseratio Aug 26 '15 at 23:13

1 Answers1

10

The reasons for confusion are these:

  1. The UI doesn't have a "special" TaskScheduler. The default case for code running on the UI thread is that TaskScheduler.Current stores the ThreadPoolTaskScheduler and SynchronizationContext.Current stores WindowsFormsSynchronizationContext (or the relevant one in other UI apps)
  2. ThreadPoolTaskScheduler in TaskScheduler.Current doesn't necessarily mean it's the TaskScheduler being used to run the current piece of code. It also means TaskSchdeuler.Current == TaskScheduler.Default and so "there is no TaskScheduler being used".
  3. TaskScheduler.FromCurrentSynchronizationContext() doesn't return an "acutal" TaskScheduler. It returns a "proxy" that posts tasks straight to the captured SynchronizationContext.

So if you run your test before starting the task (or in any other place) you'll get the same result as after the await:

MessageBox.Show(TaskScheduler.Current == TaskScheduler.FromCurrentSynchronizationContext()); // False

Because TaskScheduler.Current is ThreadPoolTaskScheduler and TaskScheduler.FromCurrentSynchronizationContext() returns a SynchronizationContextTaskScheduler.

This is the flow of your example:

  • You create a new SynchronizationContextTaskScheduler from the UI's SynchronizationContext (i.e. WindowsFormsSynchronizationContext).
  • Schedule the task you create using Task.Factory.StartNew on that TaskScheduler. Since it's just a "proxy" it posts the delegate to the WindowsFormsSynchronizationContext which invokes it on the UI thread.
  • The synchronous part (the part before the first await) of that method is executed on the UI thread, while being associated with the SynchronizationContextTaskScheduler.
  • The method reaches the await and is "suspended" while capturing that WindowsFormsSynchronizationContext.
  • When the continuation is resumed after the await it is posted to that WindowsFormsSynchronizationContext and not the SynchronizationContextTaskScheduler since SynchronizationContexts have precedence (this can be seen in Task.SetContinuationForAwait). It then runs regularly on the UI thread, without any "special" TaskScheduler so TaskScheduler.Current == TaskScheduler.Default.

So, the created task runs on the proxy TaskScheduler which uses the SynchronizationContext but the continuation after the await is posted to that SynchronizationContext and not the TaskScheduler.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • The continuation after the `await` runs on the `ThreadPoolTaskScheduler`, not the UI. – Yuval Itzchakov Aug 26 '15 at 14:35
  • @YuvalItzchakov Why do you say that? I disagree. It's posted to the UI using its `SynchronizationContext` – i3arnon Aug 26 '15 at 14:39
  • Wait, which `await` are you talking about? :) – Yuval Itzchakov Aug 26 '15 at 14:40
  • @YuvalItzchakov the one inside the task. – i3arnon Aug 26 '15 at 14:41
  • It is not clear to me why _"UI thread doesn't have a "special" TaskScheduler"_. there is a context which is the UI thread's context. no? – Royi Namir Aug 26 '15 at 14:45
  • @RoyiNamir when you check `TaskScheduler.Current` while running on the UI thread you always get the `ThreadPoolTaskScheduler`. There's no `WindowsFromTaskScheduler` you need to be on. – i3arnon Aug 26 '15 at 14:47
  • @YuvalItzchakov That's already after the continuation was scheduled on the captured `TaskScheduler` and then posted to the `SynchronizationContext`. What you're seeing is the default case for code that runs on the UI thread. – i3arnon Aug 26 '15 at 14:53
  • Then how does the thread pool scheduler know to post continuations onto the UI thread? – Yuval Itzchakov Aug 26 '15 at 14:54
  • 1
    @YuvalItzchakov That scheduler is never involved. The scheduler that does that is the `SynchronizationContextTaskScheduler` with is the one that's captured before the `await`. – i3arnon Aug 26 '15 at 14:56
  • Then what is this `WinFormsSynchronizationContext` sorcery if it doesn't actually exist? And what kind of name is `SynchronizationContextTaskScheduler`? Meaningless – Yuval Itzchakov Aug 26 '15 at 14:58
  • 1
    @YuvalItzchakov It does exist, it's just not a `TaskScheduler`. It's a `SynchronizationContext` that executes actions posted to it on the UI thread (where `TaskScheduler.Current` returns the `ThreadPoolTaskScheduler`). – i3arnon Aug 26 '15 at 15:00
  • 2
    This seems correct. WinForms never sets `TaskScheduler.Current`. The TPL does that when it processes the task created with `StartNew`. So if the TPL is not involved, and it ceases to be at the first await, `TaskScheduler.Current` reverts back to the default value. – usr Aug 26 '15 at 15:11