Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SynchronizationContext flows on Task.Run but not on await

After reading Stephen Toub's article on SynchronizationContext I'm left with a question about the output of this piece of .NET 4.5 code:

private void btnDoSomething_Click()
{
    LogSyncContext("btnDoSomething_Click");
    DoItAsync().Wait();
}
private async Task DoItAsync()
{
    LogSyncContext("DoItAsync");
    await PerformServiceCall().ConfigureAwait(false); //to avoid deadlocking
}
private async Task PerformServiceCall()
{
    LogSyncContext("PerformServiceCall 1");
    HttpResponseMessage message = await new HttpClient
    {
        BaseAddress = new Uri("http://my-service")
    }
    .GetAsync("/").ConfigureAwait(false); //to avoid deadlocking
    LogSyncContext("PerformServiceCall 2");
    await ProcessMessage(message);
    LogSyncContext("PerformServiceCall 3");
}

private async Task ProcessMessage(HttpResponseMessage message)
{
    LogSyncContext("ProcessMessage");
    string data = await message.Content.ReadAsStringAsync();
    //do something with data
}

private static void LogSyncContext(string statementId)
{
    Trace.WriteLine(String.Format("{0} {1}", statementId, SynchronizationContext.Current != null ? SynchronizationContext.Current.GetType().Name : TaskScheduler.Current.GetType().Name));
}

The output is:

btnDoSomething_Click WindowsFormsSynchronizationContext

DoItAsync WindowsFormsSynchronizationContext

PerformServiceCall 1 WindowsFormsSynchronizationContext

PerformServiceCall 2 ThreadPoolTaskScheduler

ProcessMessage ThreadPoolTaskScheduler

PerformServiceCall 3 ThreadPoolTaskScheduler

But I would expect PerformServiceCall 1 to not be on the WindowsFormsSynchronizationContext since the article states that "SynchronizationContext.Current does not “flow” across await points"...

The context does not get passed when calling PerformServiceCall with Task.Run and an async lambda, like this:

await Task.Run(async () =>
{
    await PerformServiceCall();
}).ConfigureAwait(false);

Can anyone clarify or point to some documentation on this?

like image 394
Stif Avatar asked Apr 01 '15 07:04

Stif


People also ask

Does Task run need to be awaited?

If it is some trivial operation that executes quickly, then you can just call it synchronously, without the need for await . But if it is a long-running operation, you may need to find a way to make it asynchronous.

What happens if you do not await a Task?

If you don't await the task or explicitly check for exceptions, the exception is lost. If you await the task, its exception is rethrown. As a best practice, you should always await the call. By default, this message is a warning.

Does Task run Use thread pool?

The main purpose of Task. Run() is to execute CPU-bound code in an asynchronous way. It does this by pulling a thread from the thread pool to run the method and returning a Task to represent the completion of the method.

Does await start a Task C#?

No, async await is just made to allow code to run whilst something else is blocking, and it doesn't do Task.


1 Answers

Stephen's article explains that SynchronizationContext doesn't "flow" as ExecutionContext does (although SynchronizationContext is a part of the ExecutionContext).

ExecutionContext is always flowed. Even when you use Task.Run so if SynchronizationContext would flow with it Task.Run would execute on the UI thread and so Task.Run would be pointless. SynchronizationContext doesn't flow, it rather gets captured when an asynchronous point (i.e. await) is reached and the continuation after it is posted to it (unless explicitly stated otherwise).

The difference is explained in this quote:

Now, we have a very important observation to make: flowing ExecutionContext is semantically very different than capturing and posting to a SynchronizationContext.

When you flow ExecutionContext, you’re capturing the state from one thread and then restoring that state such that it’s ambient during the supplied delegate’s execution. That’s not what happens when you capture and use a SynchronizationContext. The capturing part is the same, in that you’re grabbing data from the current thread, but you then use that state differently. Rather than making that state current during the invocation of the delegate, with SynchronizationContext.Post you’re simply using that captured state to invoke the delegate. Where and when and how that delegate runs is completely up to the implementation of the Post method.

That means in your case that when you output PerformServiceCall 1 The current SynchronizationContext is indeed WindowsFormsSynchronizationContext because you haven't yet reached any asynchronous point and you are still in the UI thread (keep in mind that the part before the first await in an async method is executed synchronously on the calling thread so LogSyncContext("PerformServiceCall 1"); happens before ConfigureAwait(false) happens on the task returned from PerformServiceCall).

You only "get off" the UI's SynchronizationContext when you use ConfigureAwait(false) (which disregards the captured SynchronizationContext). The first time that happens is on HttpClient.GetAsync and then again on PerformServiceCall.

like image 134
i3arnon Avatar answered Nov 02 '22 16:11

i3arnon