In the last few articles, we have seen how to work with asynchronous programming in C#. Although it is now easier than ever to write responsive applications that do asynchronous, non-blocking I/O operations, many people still use asynchronous programming incorrectly. A lot of this is due to confusion over usage of the
Task class in .NET, which is used in multithreaded and parallel scenarios as well as asynchronous ones. To make matters worse, it is not obvious to everyone that these are actually different things.
So let’s address this concern first.
Asynchronous vs Multithreading etc
When we use a computer, many programs are running at the same time. Before the advent of multicore CPUs, this was achieved by having instructions from different processes (and eventually threads) running one at a time on the same CPU. These instructions are interleaved, and the CPU switches rapidly from one to another (in what we call a context switch), giving the illusion that they are running at the same time. This is concurrency.
CPUs with multiple cores have the additional ability of literally executing multiple intructions at the same time, which is parallel execution. Multithreading gives the developers to make full use of the available cores; without it, instructions from a single process would only be able to execute on a single core at a time.
“Tasks which are executing on distinct processors at any point in time are said to be running in parallel. It may also be possible to execute several tasks on a single processor. Over a period of time, the impression is given that they are running in parallel, when in fact, at any point in time, only one task has control of the processor. In this case, we say that the tasks are being performed concurrently, that is, their execution is being shared by the same processor.” — Practical Parallel Rendering, Chalmers et al, A K Peters, 2002.
In .NET, we can do parallel processing by using multithreading, or an abstraction thereof, such as the Task Parallel Library. Parallel processing is CPU-bound.
Parallel processing is often contrasted with distributed processing, where the computing resources are not physically tightly coupled. This is not, however, relevant to asynchronous programming, so we will not delve into it.
Operations that take a long time to execute will typically hold control of the thread in which they are running, in what we call a blocking operation. However, if these operations involve waiting for I/O to occur (e.g. waiting for results from a file, network or database), then the I/O could occur in a non-blocking fashion without holding the thread at all during the waiting time. We say that the I/O operation is asynchronous: the thread that is waiting for it does not actually wait, but may be reassigned to do other work until the I/O operation is complete.
Asynchronous non-blocking I/O is not enabled by multithreading. In fact, Stephen Cleary goes into detail about how this works in his excellent post, “There Is No Thread“. In brief, a mechanism known as I/O Completion Ports (IOCP) is used to notify a thread that its I/O request is ready; but that thread does not need to block (or indeed run at all) during the waiting time. This is what we enable when we do an asynchronous wait by means of the
In order to write efficient code, it is fundamental to understand the nature of what the code is doing. Parallel CPU-based execution involves significant overheads in thread synchronization. It makes no sense to use
Parallel.ForEach() for I/O-bound tasks, and many are also surprised to find that executing CPU-based tasks sequentially is often faster than doing them in parallel, especially when such tasks are fine-grained and do very trivial work. In such cases, the synchronization overheads dwarf the cost of executing that code directly on the CPU on a single thread.
See also: “Asynchronous and Concurrent Processing in Akka .NET Actors“, which has a section on Asynchronous vs Concurrent Processing using simple tasks.
The Dangers of Blocking with Asynchronous Code
Asynchronous programming has two main benefits: scalability and offloading. If you block, then you are hogging resources (i.e. threads) that could be better used elsewhere. In an ASP .NET context, this means that a thread cannot service other requests (hurts scalability). In a GUI context, it means that the UI thread cannot be used for rendering because it is busy waiting for a long-running operation (so the work should be offloaded).
There are several methods which are part of the
Task API which block, such as
Result property also has the effect of blocking until the task is complete. Stephen Cleary has a table in his Async and Await intro showing these blocking API calls and how to turn them into asynchronous calls. Best practice is to
await the asynchronous equivalent of the blocking method
However, simply wasting threading resources is not the only problem with blocking. It is actually very easy to end up with a deadlock and stall your application. Stephen Cleary has an excellent explanation of how this happens, with two concise examples based on GUI applications (e.g. WPF or Windows Forms) and ASP .NET. I am only going to attempt to simplify the scenario and illustrate it with a diagram.
Consider the following code in a WPF application’s codebehind (MainWindow.xaml.cs):
This results in deadlock. Let’s see why:
awaiting it. This starts a task in fire-and-forget mode (so far).
awaits its result asynchronously. The thread does not block, since the execution has gone into “I/O mode” (technically a delay isn’t really I/O, but the idea is the same).
- In the meantime,
Button_Click()resumes execution, and calls
Wait(), effectively blocking until the result of
- The delay completes.
WaitABit()should resume, but the UI thread that it’s supposed to run on is already blocked.
- The deadlock occurs because the continuation of
WaitABit()needs to run on the UI thread, but the UI thread is blocked waiting for the result of
Note: I intentionally haven’t simplified
WaitABit() such that it just returns the delay rather than doing a single-line
await, as it would not deadlock. Can you guess why?
This example has shown blocking of the UI thread, but the concept stretches beyond GUI applications. In order to fully understand what is happening, we need to understand what a SynchronizationContext is. In short, it’s an abstraction of a threading model within an application. A GUI application needs a single UI thread for rendering and updating GUI components (although you can use other threads, they cannot touch the GUI directly). An ASP .NET application, on the other hand, handles requests using the thread pool. The SynchronizationContext is the abstraction that allows us to use the same multithreaded and asynchronous programming models across applications with fundamentally different internal threading models.
As a result, GUI applications (e.g. Windows Forms and WPF) and ASP .NET applications (those targeting the full .NET Framework) can deadlock. ASP .NET Core applications don’t have a SynchronizationContext, so they will not deadlock. Console applications won’t normally deadlockbecause the task continuation can just execute on another thread.
The deadlock occurs because the SynchronizationContext (e.g. the UI thread in the above example) is captured and used for the task continuation. However, we can prevent this from happening by using
The GUI application does not deadlock now, because a thread pool can be picked to execute the continuation. Once
WaitABit() completes, then the blocking
Button_Click() can resume on the same UI thread where it started. Any modifications to UI elements would work fine.
ConfigureAwait(false) is no replacement for doing
await all the way, it does have its benefits. Capturing the SynchronizationContext incurs a performance penalty, so if it is not actually necessary, library code should avoid it by using
ConfigureAwait(false). Also, if application code must block on an asynchronous call for legacy reasons, then
ConfigureAwait(false) would avoid the resulting deadlocks.
Of course, the real fix for our deadlock example here is really as easy as this (without
I’ve just replaced the blocking
Wait() call with an
await, and marked the method as
async as a result.
Asynchronous Wrappers for Synchronous Methods
A lot of library APIs with asynchronous methods have pairs of synchronous and asynchronous methods, e.g.
WriteAsync(), etc. If your library API is purely synchronous, then you should not expose asynchronous wrappers for the synchronous methods(e.g. by using
Stephen Toub goes into detail on why this is a bad idea in the article linked above, but I think the following paragraph summarises it best:
“If a developer needs to achieve better scalability, they can use any async APIs exposed, and they don’t have to pay additional overhead for invoking a faux async API. If a developer needs to achieve responsiveness or parallelism with synchronous APIs, they can simply wrap the invocation with a method like Task.Run.” — Stephen Toub, “Should I expose asynchronous wrappers for synchronous methods?“
You can’t use
await in properties. You can use them indirectly via an asynchronous method, but it’s a rather weird thing to do.
“[You can’t use await i]nside of a property getter or setter. Properties are meant to return to the caller quickly, and thus are not expected to need asynchrony, which is geared for potentially long-running operations. If you must use asynchrony in your properties, you can do so by implementing an async method which is then used from your property.” — Stephen Toub, Async/Await FAQ
async void methods
async void methods should only be used for top-level event handlers. In “The Dangers of async void Event Handlers“, I explain the general dangers of
async void methods (mainly related to not having a task that you can
await), but I also demonstrate and solve the additional problem of
async voidevent handlers interleaving while they run (which is problematic if you’re expecting to handle events in sequence, such as with a message queue).
- “There is no way for the caller to await completion of the method.
- “As a result of this, async void calls are fire-and-forget.
- “Thus it follows that async void methods (including event handlers) will execute in parallel if called in a loop.
- “Exceptions can cause the application to crash (see the aforementioned article by Stephen Cleary for more on this).”
— Daniel D’Agostino, The Dangers of async void Event Handlers
- Parallel is CPU-based. Asynchronous is I/O-based. Don’t mix the two. (Running asynchronous I/O tasks “in parallel” is OK, as long as you’re doing it following the proper patterns rather than using something like
- Asynchronous I/O does not use any threads.
- Blocking affects scalability and can hold higher-priority resources (such as a UI thread).
- Blocking can also result in deadlocks. Prevent them by using
awaitall the way if you can.
ConfigureAwait(false)is useful in library code both for performance reasons and to prevent deadlocks resulting from application code that must block for legacy reasons.
- For asynchronous libraries, don’t expose synchronous wrappers. There is an overhead associated with it, and the client can decide whether it’s worth doing from their end.
- You can’t have asynchronous properties, except indirectly via asynchronous methods. Avoid this.
async voidis for event handlers. Even so, if ordering is important, beware of interleaving.