Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the async/await return callchain work?

I had a situation recently where I had an ASP.NET WebAPI controller that needed to perform two web requests to another REST service inside its action method. I had written my code to have functionality separated cleanly into separate methods, which looked a little like this example:

public class FooController : ApiController
{

    public IHttpActionResult Post(string value)
    {
        var results = PerformWebRequests();
        // Do something else here...
    }

    private IEnumerable<string> PerformWebRequests()
    {
        var result1 = PerformWebRequest("service1/api/foo");
        var result = PerformWebRequest("service2/api/foo");

        return new string[] { result1, result2 };
    }

    private string PerformWebRequest(string api)
    {
        using (HttpClient client = new HttpClient())
        {
            // Call other web API and return value here...
        }
    }

}

Because I was using HttpClient all web requests had to be async. I've never used async/await before so I started naively adding in the keywords. First I added the async keyword to the PerformWebRequest(string api) method but then the caller complained that the PerformWebRequests() method has to be async too in order to use await. So I made that async but now the caller of that method must be async too, and so on.

What I want to know is how far down the rabbit hole must everything be marked async to just work? Surely there would come a point where something has to run synchronously, in which case how is that handled safely? I've already read that calling Task.Result is a bad idea because it could cause deadlocks.

like image 340
Peter Monks Avatar asked Aug 28 '14 16:08

Peter Monks


1 Answers

What I want to know is how far down the rabbit hole must everything be marked async to just work? Surely there would come a point where something has to run synchronously

No, there shouldn't be a point where anything runs synchronously, and that is what async is all about. The phrase "async all the way" actually means all the way up the call stack.

When you process a message asynchronously, you're letting your message loop process requests while your truly asynchronous method runs, because when you go deep down the rabit hole, There is no Thread.

For example, when you have an async button click event handler:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await DoWorkAsync();
    // Do more stuff here
}

private Task DoWorkAsync()
{
    return Task.Delay(2000); // Fake work.
}

When the button is clicked, runs synchronously until hitting the first await. Once hit, the method will yield control back to the caller, which means the button event handler will free the UI thread, which will free the message loop to process more requests in the meanwhile.

The same goes for your use of HttpClient. For example, when you have:

public async Task<IHttpActionResult> Post(string value)
{
    var results = await PerformWebRequests();
    // Do something else here...
}

private async Task<IEnumerable<string>> PerformWebRequests()
{
    var result1 = await PerformWebRequestAsync("service1/api/foo");
    var result = await PerformWebRequestAsync("service2/api/foo");

    return new string[] { result1, result2 };
}

private async string PerformWebRequestAsync(string api)
{
    using (HttpClient client = new HttpClient())
    {
        await client.GetAsync(api);
    }

    // More work..
}

See how the async keyword went up all the way to the main method processing the POST request. That way, while the async http request is handled by the network device driver, your thread returns to the ASP.NET ThreadPool and is free to process more requests in the meanwhile.

A Console Application is a special case, since when the Main method terminates, unless you spin a new foreground thread, the app will terminate. There, you have to make sure that if the only call is an async call, you'll have to explicitly use Task.Wait or Task.Result. But in that case the default SynchronizationContext is the ThreadPoolSynchronizationContext, where there isn't a chance to cause a deadlock.

To conclude, async methods shouldn't be processed synchronously at the top of the stack, unless there is an exotic use case (such as a Console App), they should flow asynchronously all the way allowing the thread to be freed when possible.

like image 176
Yuval Itzchakov Avatar answered Oct 15 '22 22:10

Yuval Itzchakov