Question

I'm still learning the async/await, so please excuse me if I'm asking something obvious. Consider the following example:

class Program  {

    static void Main(string[] args) {
        var result = FooAsync().Result;
        Console.WriteLine(result);
    }

    static async Task<int> FooAsync() {

        var t1 = Method1Async();
        var t2 = Method2Async();

        var result1 = await t1;
        var result2 = await t2;

        return result1 + result2;

    }

    static Task<int> Method1Async() {
        return Task.Run(
            () => {
                Thread.Sleep(1000);
                return 11;
            }
        );
    }

    static Task<int> Method2Async() {
        return Task.Run(
            () => {
                Thread.Sleep(1000);
                return 22;
            }
        );
    }

}

This behaves as expected and prints "33" in the console.

If I replace the second await with an explicit wait...

static async Task<int> FooAsync() {

    var t1 = Method1Async();
    var t2 = Method2Async();

    var result1 = await t1;
    var result2 = t2.Result;

    return result1 + result2;

}

...I seem to get the same behavior.

Are these two examples completely equivalent?

And if they are equivalent in this case, are there any other cases where replacing the last await by an explicit wait would make a difference?

Was it helpful?

Solution 3

OK, I think I figured this out so let me sum it up, in what will hopefully be a more complete explanation than the answers provided so far...

Short Answer

Replacing the second await with an explicit wait will have no appreciable effect on a console application, but will block the UI thread of a WPF or WinForms application for the duration of the wait.

Also, the exception handling is slightly different (as noted by Stephen Cleary).

Long Answer

In a nutshell, the await does this:

  1. If the awaited task has already finished, it just retrieves its result and continues.
  2. If it hasn't, it posts the continuation (the rest of the method after the await) to the current synchronization context, if there is one. Essentially, await is trying to return us where we started from.
    • If there isn't a current context, it just uses the original TaskScheduler, which is usually thread pool.

The second (and third and so on...) await does the same.

Since the console applications typically have no synchronization context, continuations will typically be handled by the thread pool, so there is no issue if we block within the continuation.

WinForms or WPF, on the other hand, have synchronization context implemented on top of their message loop. Therefore, await executed on a UI thread will (eventually) execute its continuation on the UI thread as well. If we happen to block in the continuation, it will block the message loop and make the UI non-responsive until we unblock. OTOH, if we just await, it will neatly post continuations to be eventually executed on the UI thread, without ever blocking the UI thread.

In the following WinForms form, containing one button and one label, using await keeps the UI responsive at all times (note the async in front of the click handler):

public partial class Form1 : Form {

    public Form1() {
        InitializeComponent();
    }

    private async void button1_Click(object sender, EventArgs e) {
        var result = await FooAsync();
        label1.Text = result.ToString();
    }

    static async Task<int> FooAsync() {

        var t1 = Method1Async();
        var t2 = Method2Async();

        var result1 = await t1;
        var result2 = await t2;

        return result1 + result2;

    }

    static Task<int> Method1Async() {
        return Task.Run(
            () => {
                Thread.Sleep(3000);
                return 11;
            }
        );
    }

    static Task<int> Method2Async() {
        return Task.Run(
            () => {
                Thread.Sleep(5000);
                return 22;
            }
        );
    }

}

If we replaced the second await in FooAsync with t2.Result, it would continue to be responsive for about 3 seconds after the button click, and then freeze for about 2 seconds:

  • The continuation after the first await will politely wait its turn to be scheduled on the UI thread, which would happen after Method1Async() task finishes, i.e. after about 3 seconds,
  • at which point the t2.Result will rudely block the UI thread until the Method2Async() task finishes, about 2 seconds later.

If we removed the async in front of the button1_Click and replaced its await with FooAsync().Result it would deadlock:

  • The UI thread would wait on FooAsync() task to finish,
  • which would wait on its continuation to finish,
  • which would wait on the UI thread to become available,
  • which it isn't, since it is blocked by the FooAsync().Result.

The article "Await, SynchronizationContext, and Console Apps" by Stephen Toub was invaluable to me in understanding this subject.

OTHER TIPS

Your replacement version blocks the calling thread waiting for the task to finish. It's hard to see a visible difference in a console app like that since you're intentionally blocking in Main, but they're definitely not equivalent.

They are not equivalent.

Task.Result blocks until the result is available. As I explain on my blog, this can cause deadlocks if you have an async context that requires exclusive access (e.g., a UI or ASP.NET app).

Also, Task.Result will wrap any exceptions in AggregateException, so error handling is harder if you synchronously block.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top