C# – How will C# 5 async support help UI thread synchronization issues

asynccmultithreading

I heard somewhere that C# 5 async-await will be so awesome that you will not have to worry about doing this:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

It looks like the callback of a await operation will happen in the original thread of the caller. It has been stated several times by Eric Lippert and Anders Hejlsberg that this feature originated from the need to make UIs (particularly touch device UIs) more responsive.

I think a common usage of such feature would be something like this:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

If only a callback is used then setting the label text will raise an exception because it's not being executed in the UI's thread.

So far I couldn't find any resource confirming this is the case. Does anyone know about this? Are there any documents explaining technically how this will work?

Please provide a link from a reliable source, don't just answer "yes".

Best Answer

I think you're getting a few things confused, here. What you're asking for is already possible using System.Threading.Tasks, the async and await in C# 5 are just going to provide a little nicer syntactic sugar for the same feature.

Let's use a Winforms example - drop a button and a textbox on the form and use this code:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Run it and you'll see that (a) it doesn't block the UI thread and (b) you don't get the usual "cross-thread operation not valid" error - unless you remove the TaskScheduler argument from the last ContinueWith, in which case you will.

This is bog-standard continuation passing style. The magic happens in the TaskScheduler class and specifically the instance retrieved by FromCurrentSynchronizationContext. Pass this into any continuation and you tell it that the continuation must run on whichever thread called the FromCurrentSynchronizationContext method - in this case, the UI thread.

Awaiters are slightly more sophisticated in the sense that they're aware of which thread they started on and which thread the continuation needs to happen on. So the above code can be written a little more naturally:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

These two should look very similar, and in fact they are very similar. The DelayedAddAsync method now returns a Task<int> instead of an int, and so the await is just slapping continuations onto each one of those. The main difference is that it's passing along the synchronization context on each line, so you don't have to do it explicitly like we did in the last example.

In theory the differences are a lot more significant. In the second example, every single line in the button1_Click method is actually executed in the UI thread, but the task itself (DelayedAddAsync) runs in the background. In the first example, everything runs in the background, except for the assignment to textBox1.Text which we've explicitly attached to the UI thread's synchronization context.

That's what's really interesting about await - the fact that an awaiter is able to jump in and out of the same method without any blocking calls. You call await, the current thread goes back to processing messages, and when it's done, the awaiter will pick up exactly where it left off, in the same thread it left off in. But in terms of your Invoke/BeginInvoke contrast in the question, I'm sorry to say that you should have stopped doing that a long time ago.