The .NET framework has had several different patterns for doing asynchronous work — the Task Parallel Library (TPL), the Event-based Asynchronous Pattern (EAP), and the Asynchronous Programming Model (APM), to name a few. Since they build on each other, if you’re not familiar with all of them, it can be difficult to understand how they interact.
The async/await pattern emerged a while ago as a popular solution to some previously unpleasant problems common to asynchronous code in C#.
The Good Stuff
One problem that async/await helps to solve is the “pyramid of doom” that forms from a long chain of callbacks. By using async/await, we can magically flatten the structure back out to something a little easier to read. I say “magically” because what’s really happening is that the compiler is doing some significant rewriting of the program’s structure. The runtime call stack looks nothing like the structure of the source code anymore — hence, a “little” easier to read.
Another advantage of async/await is non-blocking I/O. There’s no need for threads to sit around idle while waiting for an I/O operation to complete (e.g., disk access or network request).
The Bad Stuff
Of course, although using async/await seems simple enough at first, it is hiding quite a bit of complexity. It makes a mess of the call stack. It is more complicated to debug. Also, ever try to mix async/await with lambdas?
Still, the good usually outweighs the bad… until nearly every method in your application is async. Then it feels more like cancer. Your methods all return
Task<...> (or worse,
Task<Task<...>>). Unit tests suddenly need to become async for no reason. To control the spread, you might think to yourself that all you need to do is run your async functions synchronously. But this is a trap.
It turns out there really is no reliable way to run an asynchronous method synchronously. This crazy StackOverflow thread perfectly illustrates the seemingly innocent attempts to do so.
Stephen Cleary has written many excellent blog posts explaining async/await (therefore, I will not try to do so here). When he says, “The best answer to the question, ‘How can I call an async method synchronously?’ is, ‘Don’t!'” I believe him. I also wish I had read his intro to Async and Await (from 2012, but still relevant) a long time ago.
So what can you do? It depends on your primary motivation for using async/await. Is it to avoid blocking the thread? In a UI application, this is important for the main thread, but not so much for other threads. In ASP.NET, quickly returning threads to the thread pool is important for very high throughput. But if you’re not serving a lot of traffic, it might not be an issue. And if you’re looking at how to run an async method synchronously, you must not think it’s an issue anyway.
Sometimes an asynchronous call is not necessary. If there’s a synchronous version of a library method already available and it will complete quickly, consider using that instead.
If you like async/await for eliminating the pyramid of doom, you can at least try to isolate asynchronous calls. When it really is necessary to
await something, aim to do it near the top of your hierarchy (by “top” I mean something like an event handler in a UI application or controller method in ASP.NET). This will at least save intermediate methods from becoming async unnecessarily.
But avoid the temptation to halt the spread of async by declaring a method as
async void. By not returning a
Task, the caller will be unable to
await it (which might be fine) and will also be unable to catch exceptions.
It would be awesome if async/await actually reduced code complexity instead of just hiding that complexity somewhere else. But sometimes complexity is unavoidable, and it becomes a matter of making it manageable.