Why Use Async/Await All the Way Down

Why use Async/await all the way down

A better slogan is async all the way up. Because you start with an asynchronous operation and make its caller asynchronous and then the next caller etc.

You should use async-await when you have an inherently asynchronous operation (usually I/O but not necessarily) and you don't want to waste a thread idly waiting for the operation to complete. Choosing an async operation instead of a synchronous one doesn't speed up the operation. It will take the same amount of time (or even more). It just enables that thread to continue executing some other CPU bound work instead of wasting resources.

But to be able to await that operation the method needs to be an async one and the caller needs to await it and so forth and so forth.

So async all the way up enables you to actually make an asynchronous call and release any threads. If it isn't async all the way then some thread is being blocked.

So, this:

async Task FooAsync()
{
await Func1();
// do other stuff
}

async Task Func1()
{
await Func2();
// do other stuff
}

async Task Func2()
{
await tcpClient.SendAsync();
// do other stuff
}

Is better than this:

void Foo()
{
Func1();
// do other stuff
}

void Func1()
{
Func2().Wait(); // Synchronously blocking a thread.
// do other stuff
}

async Task Func2()
{
await tcpClient.SendAsync();
// do other stuff
}

Async All the Way Down: Well, what's all the way at the bottom?

Every procedural programming language is a series of function / procedure calls, with one function / procedure calling another. It is possible to represent this sequence of calls from procedure to procedure using a call graph see Wikipedida for a starting point for call graphs. These graphs generally show the sequence of procedural calls starting at the top and proceeding to the bottom.

I quickly produced a diagram of a partial call graph for an ASP .NET MVC application. The diagram is only a partial representation for an ASP .NET MVC application because it omits things such as the initial receipt of the request by the operating system (eg. Windows), web server (eg. IIS) and various components of ASP .NET that are responsible for processing a HTTP request as it travels through the ASP .NET Request processing pipeline. These matters can can be omitted for the purposes of this discussion. Although it's worthwhile noting that they would sit at the top of the call graph, because they deal with the initial stages of handling the HTTP request, and ultimately, at some point ASP .NET ultimately invokes an action of a controller.

As you can see in the diagram I've represented the action of the controller that would be invoked by ASP .NET as an async action. On the left hand side of the call graph is a series of async procedure calls. Ultimately it arrives at the box that is the subject of your question.

The answer to the question that is consistent with the notion "All the way down" is that "I am an async method". What does this async method do? Well, that depends on what you want to do? If you're reading or writing a file then it's an async call to read or write a file. Are you making a database query? Then the call is to an async method that makes that query. With this in mind I guess you can say that often what is at the bottom will be a device driver method, that performs an IO access asynchronously. Although it could easily be a long running compute bound asynchronous operation that you wish to perform such as processing an image, or video file.

It's worthwhile noting the right hand side of the call graph here too. Although often you may want to call async methods all the way down, the right hand branch of this call graph shows that you don't necessarily need to call an async method at the bottom at all.
Sample Image

c# async await confuses me, seems to do nothing

This will hopefully demonstrate it better:

static async Task Main(string[] args)
{
var timer = Stopwatch.StartNew();
var t1 = LongTask();
var t2 = LongTask();
Console.WriteLine("Started both tasks in {0}", timer.Elapsed);
await Task.WhenAll(t1, t2);
timer.Stop();
Console.WriteLine("Tasks finished in {0}", timer.Elapsed);
}

async static Task LongTask()
{
await Task.Delay(2000);
}

Notice the change in return types of functions. Notice the output, too. Both task are waiting for something (in this case delay to expire) in parallel and at the same time, the Main function is able to continue running, printing text and then waiting for the outcome of those functions.

It is important to understand that the execution of an async function runs synchronously until it hits the await operator. If you don't have any await in an async function it will not run asynchronously (and compiler should warn about it). This is applied recursively (if you have several nested awaits) until you hit something that is actually going to be waited for. See example:

static async Task Main(string[] args)
{
var timer = Stopwatch.StartNew();
var t1 = LongTask(1);
Console.WriteLine("t1 started in {0}", timer.Elapsed);
var t2 = LongTask(2);
Console.WriteLine("Started both tasks in {0}", timer.Elapsed);
await Task.WhenAll(t1, t2);
timer.Stop();
Console.WriteLine("Tasks finished in {0}", timer.Elapsed);
}

async static Task LongTask(int id)
{
Console.WriteLine("[{0}] Before subtask 1", id);
await LongSubTask1();
Console.WriteLine("[{0}] Before subtask 2", id);
await LongSubTask2();
Console.WriteLine("[{0}] After subtask 1", id);
}

async static Task LongSubTask1(int id)
{
Console.WriteLine("[{0}] LongSubTask1", id);
await Task.Delay(1000);
}
async static Task LongSubTask2(int id)
{
Console.WriteLine("[{0}] LongSubTask2", id);
await Task.Delay(1000);
}

If you run it, you will see that the execution runs synchronously all the way down to Task.Delay call and only then it returns all the way back to the Main function.

In order to benefit from async/await, you have to use it all the way down to where it ends up calling some I/O (network, disk, etc.) you will need to wait for (or some other event that does not require constant polling to figure out, or artificially created like delay in that sample). If you need to do some expensive computation (that does not have anything async in it) without occupying some particular thread (say, UI thread in GUI application), you'd need to explicitly start new task with it, which at some later point you can await for to get its result.

Is it OK to use async/await almost everywhere?

async/await is sometimes called "contagious" or "viral" (or so it has in the C# world), because in order for it to be effective, it needs to be supported all the way down the call chain. Forcing something asynchronous to act synchronous can lead to unintended results, so you should extend it from the original method all the way down to the top level consumer using it. In other words, if you create or use a type that uses it, that type should also implement it, and so on all the way up the chain. So yes, it's expected that you add async to every function that itself relies on it. Just note, however, you should not add preemptively add async to functions that don't actually implement or need it.

Just think: If you use async (by awaiting something, I mean), you are async. Avoid squashing an async call into something synchronous.

Do I have to await all the way down the call chain?

You do if you want the promise to be resolved.

Because

const result = bar();

is perfectly valid: it returns a promise.

Using await, you instruct the engine to wait for the promise to be resolved (or fail) and get the result. There are many valid cases where you would want to deal with the promise itself (for example adding operations) even inside an async function.

Async all the way down?

Both versions work effectively the same, the only difference is that when you use await here, you get some performance penalty (because the state machine must be set up and a continuation will most likely be used).

So, it comes down to a tradeoff: Do you want your methods to be somewhat more efficient at the cost of being slightly less readable? Or are you willing to sacrifice performance for readability?

Usually, I would advise you to go for readability first and only focus on performance if profiling tells you it's worth it. But in this case, I think the increase in readability is small, so I would probably not use await.

Also note that your class C still doesn't go far enough: foo1() also doesn't need await.



Related Topics



Leave a reply



Submit