Click here to Skip to main content
15,891,783 members
Articles / Programming Languages / C#

A Surprise Synchronization Context Gotcha

Rate me:
Please Sign up or sign in to vote.
4.80/5 (3 votes)
12 Mar 2015CPOL4 min read 9.5K   9   4
This is a post about a surprise synchronization context gotcha.

I got into an interesting argument conversation with a co worker last week about whether async/ await was multi-threaded. He thought I was bonkers for suggesting it was not multi-threaded. So I did some research.

First off, obviously if you're doing async/await, it's probably because you want some multithreaded behavior like network IO or file IO where some other thread does some work for you while freeing your UI thread to handle UI stuff (or in the case of IIS, releasing your thread to handle other incoming requests, thus giving you better throughput). So my co-worker was right that 99% of the time, async/await will probably involve multiple threads.

However, if async/await were multi-threaded by its very nature, then it should be impossible to write a program using async/await that was single-threaded. So let's try to write a method that we can prove is single-threaded that also uses async/await. How about this:

C#
public async void HandleClickEvent()
{
    await Task.Yield();
    j = 1;
    while (j != 0)
    {
        if (j == -1) j++;
        j++;
    }
    await Task.Yield();
}

It took some work to come up with an infinite loop that looked normal to the compiler, but that's what the while loop is doing. If async/await were multi-threaded, then we might think that the UI thread would hit the first Task.Yield and spawn off a new thread. Then the infinite loop would be run on a new thread and the UI would work great, right?

If we actually run that code in a Windows Store app the UI freezes. Why? Because, according to MSDN:

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.

So when I claimed async/await wasn't multi-threaded, I was thinking of that. What's basically happening is that the UI thread is a message pump that processes events, and when you await within the UI thread's synchronization context, you yield control to the UI thread's message pump, which allows it to process UI events and such. When your awaited call returns, it throws an event back to the UI thread's message pump and the UI thread gets back to your method when it's done with anything else it's working on.

But after some research, I realized that I didn't know nearly enough about synchronization contexts and so I spent the morning reading about them. After a lot of research, I finally found someone that has a great description of how all this works under the covers and if you get the chance, I highly recommend reading C# MVP Jerome Laban's awesome series C# 5.0 Async Tips and Tricks.

In particular, one thing I learned is that if you start a new Task, you throw away the UI thread's synchronization context. If you await when there is no synchronization context, then by default WinRt will give you some random thread from the thread pool, which may be different after each await. In other words, if you do this:

C#
public async Task RefreshAvailableAssignments()
{
    await Task.Run(async () =>
    {
        Debug.WriteLine(Environment.CurrentManagedThreadId);
        await Task.Yield();
        Debug.WriteLine(Environment.CurrentManagedThreadId);
    });
}

You will (usually) get a different thread after the yield than you did before it. That can lead to trouble if you aren't careful and aren't aware of it. It can be especially dangerous if you're deep in the guts of something and you aren't 100% sure of whether you are being called from the UI thread or from some other thread. It can be particularly bad if someone after you decides to put your code into a Task.Run and you were dependent upon the UI thread's synchronization context without being aware of it. Nasty, huh?

It makes me like more and more the idea introduced in the post by Jason Gorman entitled Can Restrictive Coding Standards Make Us More Productive? where he describes ways of discouraging team members from starting new threads (or Tasks on my project since WinRt doesn't give us Threads) unless there is a really good reason for doing so.

It goes back to a most excellent statement my co-worker made:

Async/await is very powerful, but we all know what comes with great power.

So that was fun. I look forward to having lots more constructive arguments conversations like this one in the future. :)

This article was originally posted at http://www.leerichardson.com/feeds/posts/default

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Lee is a Microsoft MVP and a prolific writer, speaker, and youtuber on .Net and open source topics. Lee is a Solution Samurai at InfernoRed ( http://infernoredtech.com). When not coding he enjoys running, mountain biking, smoking brisket, electronics, 3D printing, and woodworking. He is active on twitter where you can reach him @lprichar ( https://twitter.com/lprichar).

Comments and Discussions

 
SuggestionBottom line Pin
ArchAngel12313-Mar-15 9:46
ArchAngel12313-Mar-15 9:46 
QuestionSynchronizationContext Pin
Paulo Zemek12-Mar-15 7:35
mvaPaulo Zemek12-Mar-15 7:35 
AnswerRe: SynchronizationContext Pin
Lee P Richardson12-Mar-15 8:22
Lee P Richardson12-Mar-15 8:22 
GeneralRe: SynchronizationContext Pin
Paulo Zemek12-Mar-15 9:06
mvaPaulo Zemek12-Mar-15 9:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.