Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

await does not resume context after async operation?

Tags:

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 ?

like image 920
Royi Namir Avatar asked Aug 26 '15 14:08

Royi Namir


People also ask

Does await pause the thread?

A good way to think about this is to imagine that asynchronous methods have “pause” and “play” buttons. When the executing thread reaches an await expression, it hits the “pause” button and the method execution is suspended.

Is it OK to not await async?

Yes. If you don't need to wait, don't wait.

How does async and await work internally?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method.


1 Answers

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.

like image 72
i3arnon Avatar answered Sep 21 '22 01:09

i3arnon