Asynchronous programming — tips and tricks
The appearance of async/await patterns in C# introduced the new ways of writing good and reliable parallel code, but as it always happen with innovations, it also introduced the new ways of fitting a square peg into a round hole. Very often when trying to solve multithreading problems with async/await, programmers are not just not solving old ones, but creating new, when deadlocks, starvations and race conditions are still there but it’s even harder to find them.

So I just thought about sharing some of my experience here. Maybe it will make someone’s life easier. But first, let’s take a short history course first and see how did async/await pattern appeared in our lifes and what problems can we solve with it’s help. It all started with callbacks, when we just have a function and as a second action we pass action, which is called afterwards. Something like:
void Foo(Type parameter, Action callback) {...}void Bar() {
some code here
...
Foo(parameter, () => {...});
}
that was quite cool, but there was a tendency for these callbacks structures to grow enormously.

Then there was Microsoft APM (Asynchronous Programming Model) which also had a more or less similar problems with callback, exceptions and was still not clear in which thread code is executed. Another step on the way was an implementation of EAP Event based Asynchronous Pattern. EAP introduced already known Async naming convention where the simplest classes may have a single MethodNameAsync method and a corresponding MethodNameCompleted event, but still felt a lot like callbacks. This is interesting because it shows that now everything that has Async in it’s name returns the Task.
public class AsyncExample
{
// Synchronous methods.
public int Method1(string param);
public void Method2(double param);
// Asynchronous methods.
public void Method1Async(string param);
public void Method1Async(string param, object userState);
public event Method1CompletedEventHandler Method1Completed;
public bool IsBusy { get; }
}
Finally we come to the TAP or Task Asynchronous Pattern which we all love and know, right? Asynchronous methods in TAP include the Async
suffix after the operation name for methods that return awaitable types, such as Task, Task<TResult>, ValueTask, and ValueTask<TResult>. With this approach a Task object was introduced which gave us several benefits, compering to patterns above.
- States of code execution, like
Cancelled, Faulted, RanToCompletion
- Explicit Task cancellation with
CancellationToken
TaskScheduler
which helps with the code execution context
Now, with that short history introduction, let’s jump to more practical things which can make our life a little easier. I will just try and mention a few most important, in my opinion, practices which i used most often.
.ConfigureAwait(false)
Quite often I’ve heard from the colleagues and also read in the posts, that you have problems with deadlocks, just use .ConfigureAwait(false)
everywhere and you’ll be fine and i can’t really agree to that. While using .ConfigureAwait(false)
can be really beneficial it is always important to keep some things in mind. For example, you should not use ConfigureAwait
when you have code after the await in the method that needs the context. CLR controls in which thread code will be executed after await
keyword and with .ConfigureAwait(false)
we’re basically saying that we don’t care in which thread code will be executed after await
keyword. That means if we do manipulations with UI or in ASP.Net we do something with HttpContext.Current
or building http response we always need to continue execution in main thread. However, if you’re writing a library and you are not sure how the library will be used, it is a good idea to use .ConfigureAwait(false)
— by using ConfigureAwait
in this manner, you enable a small amount of parallelism: Some asynchronous code can run in parallel with the main thread instead of constantly badgering it with bits of work to do.
CancellationToken
Usage of CancellationToken
class with tasks is a good idea in general, this mechanism is an easy and useful tool for controlling task execution flow, which is useful especially with a long executing methods which may be stopped by the user. It might be a heavy calculation process, a long running database request or just a network request.
Please note, there are a couple of types of cancellation exceptions: TaskCanceledException and OperationCanceledException. TaskCanceledException derives from OperationCanceledException. So, when writing a catch blocks that deal with the fallbacl of a canceled operation, it is better to catch OperationCanceledException, otherwise some cancellation occurrences slip through your catch blocks and lead to unpredicted results.
.Result / Wait()
With this ones it is very simple — just try to avoid it, unless you are 100% sure on what you are doing, but then still just await
. Microsoft says that Wait(TimeSpan)
is a synchronization method that causes the calling thread to wait for the current task instance to complete.
Remember we mentioned that Task is always executed in a context and clr controls in which thread continuation will be executed? Take a look on a code below:
- In the controller we call GetJsonAsync (ASP.NET context).
- The http request _httpClient.GetStreamAsync(“https://really-huge.json") is started
- then GetStreamAsync returns an uncompleted Task, indicating that request is not complete.
- GetJsonAsync awaits the Task returned by GetStreamAsync. The context is saved and will be used to continue running the GetJsonAsync method. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not completed yet.
- By using
jsonTask.Result.ToString();
in the controller we synchronously blocking the Task returned by GetJsonAsync. This blocks the main context thread. - At some point GetStreamAsync will be finished and it’s Task will be completed, after that GetJsonAsync is ready to continue, but it waits for the context to be available so it can execute in the context. Then we have a Deadlock, because controller method is blocking the context thread, while waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete.
This kind of deadlocks are not easy to spot and often may cause a lot of discomfort that’s why usage of Wait() and .Result isn’t recommended.
Task.Yield()
This one is a little trick which i, honestly, didn’t use much, but it is good to have in you arsenal. The thing with this one is that when async
/await
is used, there is no guarantee that when you do await FooAsync()
will actually run asynchronously. The internal implementation is free to return using a completely synchronous path. Imagine we have some method:
Looks like we’re not blocking anything here and we expect await AnotherMethodAsync();
to run asynchronously as well, but let’s take a look what will happen behind the scenes. When our code is compiled we can get something like the code below (very simplified):
Here’s what happens:
- someSynchronousCode() will run synchronously as expected.
- then
AnotherMethodAsync()
is synchronously executed, then we get an awaiter object with .GetAwaiter() - by checking the awaiter.IsCompleted we can see if task was or wasn’t completed
- if task is completed then we just run the continuationCode() synchronously
- if task is not completed, then continuationCode() is scheduled to be executed in the task context
As a result even if using await
the code can be still executed synchronously and by using await Task.Yield()
we can always guarantee !awaiter.IsCompleted
and force the method to complete asynchronously. Sometimes it can make a difference, for example in UI thread we can make sure that we’re not keeping it busy for a long time.
ContinueWith()
It is common to have a situation when after one operation is completed,we need to invoke a second operation and pass data to it. Traditionally, callback methods were used to run the continuations. In the Task Parallel Library, the same functionality is provided by continuation tasks. The Task type exposes multiple overloads of ContinueWith
. This method creates a new task that will be scheduled when another task completes. Let’s look on a very common case where we’re downloading images and doing some processing on every image. We need to do the processing sequentially, but we want to download with as much concurrency as possible, but also execute computationally-intensive processing on the ThreadPool
of the downloaded images:
in this case it is very handy to use ContinueWith()
because unlike the code after await
, ContinueWith()
logic will be executed on a thread pool, which is what we need for a have computing and to prevent blocks on a main thread.
Please note that Task.ContinueWith
passes the reference on the previous object to the user delegate. If the previous object is System.Threading.Tasks.Task<TResult> object and the task ran to completion, we can then Task<TResult>.Result property of the task. Indeed the .Result property blocks until the task has completed, but ContinueWith()
is also invoked by another Task when the task status has changed. That is why we check the status first and only then do the processing or log the error.
The big part of this article was inspired by TAP document and DotNext conference. So feel free to read the document if you want to know more.
GL & HF