Async Programming Basics
2022-02-12
0. What is Async Programming?
Async Programming means our task will be running on a different thread and once it is completed, the original thread will continue with the information gathered from the Async task.
Why do we need Async Programming?
- Usually we don’t want to block UI while doing some time consuming tasks
- Sometimes we want to make sure the server can do some other tasks while waiting for the I/O operation to be completed
- In short, increase user experience as well as make our application more efficient.
Difference between Async Programming and Parallel Programming
Async Programming has continuaty, the current thread will subscribe to the Async Task and continue the execution from there.
Parallel Programming are often used to utilize CPU by dividing a problem into smaller pieces that are solved independently.
Basically, they are solving different problems.
But in C#, they can both use Task Parallel Library, so there might be some confusion around it.
1. Implementation
Async/Await keywords
They usually go hand in hand, if you have an async method, the caller must await for it.
Async void is bad
Because we cannot catch exceptions and deal with them.
We should return Task instead.
Method signature
We don’t need to add Async in the method name.
Chaining
We can use ContinueWith to chain tasks.
Reflection of the async/await method chain
The underlining implementation of Async/Await is to create a state machine for the method, bear this in mind and be cautious of chaining multiple async/await calls, it might introduce performance issue.
Note: Even though async and await introduces overhead, it ensures that the code is awaited and makes it potentially easier to change, as you can expand with code in the continuation.
If there’s nothing required in continuation, then a better solution is to keep the method signature without async/await, and only the first method is marked as async (only top level uses async/await).
e.g., this is an example of multiple state machines caused by a method chain.
async Task Main()
{
await Run();
}
public async Task<string> Run() {
return await Compute();
}
public async Task<string> Compute() {
return await Load();
}
public async Task<string> Load() {
return await Task.Run(() => "Hi".Dump());
}
It can be simplified as the following:
async Task Main()
{
await Run();
}
public Task<string> Run() {
return Compute();
}
public Task<string> Compute() {
return Load();
}
public Task<string> Load() {
return Task.Run(() => "Hi".Dump());
}
2. Common Considerations
- How to get the result and use it? (use await and then use the result)
- How to handle exceptions? (try catch blocks)
- How to handle success and failure tasks? (check operator)
- How to resume the UI thread? (use Dispatcher.Invoke)
- How to cancel and stop tasks? (original task & chained tasks)
- If we have multiple async calls running in parallel, how do we know one or all of them have been completed? (put await in a proper place and use WhenAll/WhenAny to check)
- How to test async methods? (use interface/strategy pattern and add a mock class to implement the interface)
- How to process parallel tasks once each call finishes (use ContinueWith to access the result)
- How to create a thread safe list to hold the parallel tasks’ result (use ConcurrentBage
) - What to do if you don’t care about the original context (set ConfigureAwait to false)
- How to use stream async to improve user experience? (AsyncEnumerable, yield, observableCollection, file stream reaader, AsyncDisposable)
3. Common Issues
- deadlock: caused by calling result or wait before
await
. - mess up with the threads: if we have 3 tasks, A is cancelled, B is continue with A, C is continued with A, then both B and C knows A’s status; but if we chain C with B, C will only check B’s status, not A’s.
4. Tips
You can turn on the Visual Studio debugger thread window to check the threads easily.
- Menu -> Debug -> Windows -> Threads