In my distant past I was involved in a lot of multi-threaded development. The basic idea was that while using an application, lets say a desktop Win Forms app, you may need to kick off a task that has to run in the background for an extended length of time. In order to do this we would create a thread to execute tasks to ensure we were not impinging on the responsiveness of the UI.
This model for development worked very well for years, however, OS threads are considered relatively expensive items and as such we only tended to employ threads in this way when we had complex or long running activities to execute. Additionally the hardware we had access to meant that we were limited in handling several hundred threads which in turn means we could naturally handle several hundred requests concurrently.
Two things changed over the last dozen years or so:-
- Mobile devices with limited CPU resources became ubiquitous, which meant few available threads for popular apps.
- An increase in apps and services which require tens of thousands of “simple“ tasks executed simultaneously.
To match this reality our programming models evolved to make more effective use of all the processors via the Task Process Library (TPL).
Why we pivoted from Threads to Tasks
When a process starts, the common language runtime automatically creates a thread to execute application code. The process can at this point create one or more threads to execute the program code associated with the process. These threads can execute either in the foreground or in the background. Thread resources are limited so not only are these threads expensive but a thread that is not active is directly wasting system wide resources, and this is where the TPL rocks.
I tend to think of the TPL as a car rental company, if you are visiting a city for a day or two, there is no point in purchasing a car in that city, you just need access to a vehicle for a few short trips, and then you want to give it back. No need to think about maintenance, that is the responsibility of the rental company.
Central to the TPL is concept of the Task which is an action that can be executed asynchronously or independently of the rest of the program. It is equivalent to a thread, but without the overhead of creating one. Tasks get queued by a Task Manager and are queued to run on multiple OS threads in a thread pool.
Some basics about the TPL
- The async keyword enables the await keyword (and manages the method results).
- The beginning of an async method is executed just like any other method. It runs synchronously until it hits an “await” (or throws an exception).
- The “await” keyword is where things can get asynchronous.
- Await is like a unary operator: it takes a single argument an "awaitable".
- Await examines that awaitable to see if it has already completed.
- If the awaitable has already completed, then the method just continues running (synchronously, just like a regular method).
- If “await” sees that the awaitable has not completed, then it acts asynchronously.
- The async method pauses until the awaitable is complete (this is where it waits), but the thread is not blocked (asynchronous by definition).
- When the awaitable completes, it goes on to execute the remainder of the async method.
- There are two awaitable types in the .NET framework:
- You can await the result of an async method that returns Task, not because it’s async, but because the method returns Task.
- You can also create your own awaitable, but I do not know much about this yet.
- Async methods can return
Task<T>, or void.
- In almost all cases, you want to return
- Why return
Task<T>? Because they’re awaitable, and void is not.
- You have to return void when you have async event handlers.
- Async methods returning Task or void do not have a return value.
- Async methods returning
Taskmust return a value of type T.
There is more to discuss here but I wanted to establish some basics before I dive into tools that might help you figure out the health of your async applications.