6
votes

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?

1
The ConfigureAwait() call won't have any affect until the Task actually starts to wait. That didn't happen yet, your LogSyncContext() call was to early. Move it after the await.Hans Passant
Isn't that deadlocking on ` DoItAsync().Wait();`?Paulo Morgado
No, it's not deadlocking thanks to the ConfigureAwait callStif

1 Answers

8
votes

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.