- Item 27: Use Async Methods for Async Work
- Item 28: Never Write async void Methods
- Item 29: Avoid Composing Synchronous and Asynchronous Methods
- Item 30: Use Async Methods to Avoid Thread Allocations and Context Switches
- Item 31: Avoid Marshalling Context Unnecessarily
- Item 32: Compose Asynchronous Work Using Task Objects
- Item 33: Consider Implementing the Task Cancellation Protocol
- Item 34: Cache Generalized Async Return Types
Item 30: Use Async Methods to Avoid Thread Allocations and Context Switches
It’s all too easy to begin thinking that all asynchronous tasks represent work being done on other threads. After all, that is one use for asynchronous work. But, in fact, many times asynchronous work doesn’t start a new thread. File I/O is asynchronous, but uses I/O completion ports rather than threads. Web requests are asynchronous, but use network interrupts rather than threads. In these instances, using async tasks frees a thread to do useful work.
When you offload work to another thread, you free up one thread at the cost of creating and running another. That’s a wise design only when the thread you are freeing up is a scarce resource. In a GUI application, the UI thread is a scarce resource: Only one thread interacts with any of the visual elements the user sees. However, thread pool threads are neither unique nor scarce (although they are limited in number); that is, one thread is the same as any other thread in the same pool. For that reason, you should avoid CPU-bound async tasks in non-GUI applications.
To explore this issue further, let’s start with GUI applications. When the user initiates an action from the UI, she expects the UI to remain responsive. It won’t be if the UI thread spends seconds (or more) performing the last action. The solution to this problem is to offload that work to another resource so that the UI can remain responsive to other user actions. As you saw in Item 29, these UI event handlers are one of the locations where async over sync composition makes sense.
Now let’s move on to console applications. Console applications that perform only one long-running CPU-bound task will not benefit from executing that work on a separate thread. The main thread will be synchronously waiting, and the worker thread will be busy. In such a case, you tie up two threads to do the work of one.
However, if a console application performs several long-running CPU-bound operations, it may make sense to run those tasks on separate threads. Item 35 discusses several options to run CPU-bound work on multiple threads.
That brings us to ASP.NET Server applications, which seem to generate a great deal of confusion among developers. Ideally, you want to keep threads free so that your application can handle a greater number of incoming requests. That leads to a design where you would offload the CPU-bound work to different threads in your ASP.NET handlers:
public async Task<IActionResult> Compose()
var model = await LongRunningCPUTask();
Let’s examine the details of what happens in this situation. By starting another thread for the task, you allocate a second thread from the thread pool. The first thread has nothing to do and can be recycled and given more work, but that requires more overhead. To “bring you back where you were,” the SynchronizationContext must keep track of all the state for this Web request and, when the awaited CPU-bound work completes, restore that state. Only then can the handler respond to the client.
With this approach, you haven’t freed any resources, but you’ve added two context switches when processing a request.
If you have long-running CPU-bound work to do in response to Web requests, you need to offload that work to another process or another machine so as to free up the thread resource and increase your Web application’s ability to service requests. For example, you might have a second Web job that receives CPU-bound requests and executes them in turn. Alternatively, you might allocate a second machine to the CPU-bound work.
Which option is the fastest depends on the characteristics of your application: the amount of traffic, the time needed to do the CPU-bound work, and network latency. You must measure these items to make an informed decision. One of the configurations you should measure is doing all the work in the Web application synchronously. This approach will likely be faster than offloading the work to another thread in the same thread pool and process.
Asynchronous work seems like magic: You offload work to another location, and then pick up your processing after it completes. To ensure the efficiency of this approach, you need to make sure that when you offload work, you free up resources rather than simply switch contexts between similar resources.