Bending the Clean Architecture Principles
Async await meme
Introduction
Imagine you’re a chef in a kitchen full of ingredients, some fresh, some a bit past their prime, all thanks to Microsoft’s “We never throw anything away” policy. This is what programming asynchronously in C# is like — an overwhelming mix of new and old syntax all in one big pot.
This pot can turn to be a big ball of code mud, you could end up with the coding equivalent of food poisoning: thread starvation, deadlocks, or even the dreaded application crashes. Most of these problems boil down to a lack of understanding of the underlying concepts such as state machines, the thread pool, context switching etc...
I drafted a document for my team explaining those concepts and decided to narrow it down to a simple 10 rule cook book here as those are pretty common in the industry. There’s a lot more than 10 rules, feel free to let me know if you want the rest!
1) Do not use Async void
From all the rules, this one is the most critical potentially crashing your whole application. async void
can’t be tracked and if any exception is triggered, it will crash the whole application.
In a previous project, this caused our whole API to crash randomly on production because of an async void
present in a background service.
public void BadAddUserAsync(User user); // You should not do this
public Task GoodAddUserAsync(User user); // Do this instead!
2) Use ValueTask or Task.FromResult for easy computations
The below example is considered bad, wasting a thread pool thread to return something that does not need an async call.
public Task<int> BadComputeDiscountAsync(User user) => Task.Run(()
=> 0.3 * user.BaskedAmount);
You should definitely use ValueTask<T>
here to avoid using an extra thread AND save a minor Task allocation.
public Task<int> BestComputeDiscountAsync(User user)
=> new ValueTask<int>(0.3 * user.BaskedAmount);
ValueTask
is relatively new and not available in every case. Task.FromResult
is your default if the first one didn’t work out. It will still allocate a Task object on the heap but that’s still far better than wasting a whole thread:
public Task<int> BestComputeDiscountAsync(User user)
=> Task.FromResult(0.3 * user.BaskedAmount);
3) Use await instead of ContinueWith
ContinueWith
is part of the old ways to deal with tasks. C# had Task
long before async/await and this was the only way to deal with it before. Now, because of how the state machine work and how powerful it is in capturing context. It is better to use async/await instead of ContinueWith()
.
public Task<int> BadAddUserAsync(User user)
{
return _client.AddUser(user).ContinueWith(task =>
{
return ++totalUserCount;
});
}
public async Task<bool> GoodAddUserAsync(User user)
{
await _client.AddUser(user);
return ++totalUserCount;
}
4) Task.WaitAsync over Task.Delay
Task.WaitAsync
supports passing down a cancellation token instead of only creating a task and failing it after a delay. This is useful when you want to add cancellation or timeout ability for async methods, that inherently don’t provide such capability.
5) Use new Thread() instead of Task.Run for long running processes
The advantage of having a managed thread pool is to manage and reuse tasks on the fly and not waste resources. A Task.Run
enqueue a task to the thread pool and leaves the thread to be reused for another call later on.
If you’re doing a long running background process with Task.Run
, you’re effectively “stealing” a thread that was meant to be used for multiple asynchronous operations such as timer callbacks and so on.
Blocking this thread will make the Thread Pool grow unnecessarily. A good alternative is to create a Thread
manually and let it live on on its own!
public class HealthCheckService : IBackgroundService
{
public async Task BadHandleAsync(Client client, CancellationToken token)
{
while(true)
{
await Task.Run(()=> client.CheckHealth(token));
await Task.Delay(500);
}
}
public Task GoodHandleAsync(Client client, CancellationToken token)
{
var t = new Thread(()=> client.CheckHealth(token))
{
IsBackground = true //important to keep it alive
};
t.Start();
return Task.CompletedTask;
}
}
6) Do not use Task.Result or Task.Wait
The problem with both if not used correctly is that the caller will be blocked until the asynchronous operation is executed. This uses 2 threads for a synchronous call instead of 1 which leads to thread starvation and can cause deadlocks. This is a common anti-pattern called Sync over Async
, you can find a detailed explanation here.
public bool BadAddUser1(User user)
=> _client.AddAsync(user).Result; //Do not do this
public bool BadAddUser2(User user) //Do not do this
{
var addUserTask= _client.AddAsync(user);
addUserTask.Wait();
return addUserTask.Result;
}
public async Task<bool> GoodAddUserAsync(User user) //Do this instead
=> await _client.AddAsync(user);
Another very common case is Timer callbacks, the default Timer
class in .NET has void
in it’s signature which make things tricky to use.
public class TimerHealthCheckService
{
private readonly Timer _badTimer;
public TimerHealthCheckService()
=> _badTimer = new Timer(BadCheckHealth, 100, 200);
// this is bad
public void BadCheckHealth() => client.CheckHealth(...);
// this is a bit better
public void BetterCheckHealth() => DoHealthCheck();
private async Task DoHealthCheck()=> await client.CheckHealth(...);
}
An alternative is to use the PeriodicTimer that supports a Task instead.
public class BetterTimerHealthCheckService
{
private readonly PeriodicTimer _timer;
public BetterTimerHealthCheckService()
{
_timer = new PeriodicTimer(100);
Task.Run(DoHealthCheck);
}
private async Task DoHealthCheck()
{
while(await _timer.WaitForNextTickAsync())
await client.CheckHealth(...);
}
}
7) Always pass down cancellation tokens
There are cases, especially in APIs where passing down cancellation tokens will help you save a lot of unused threads when a cancellation occur. Always spread this parameter all the way down to your functions. Most Async calls supports it!
8) Do not use Action parameter alone
public class MessagePublisher
{
public static void Publish(Action action)
{
//...
}
}
var publisher = new MessagePublisher();
publisher.Publish(async () =>
{
await _client.PublishAsync(messages);
});
This is creating an async void
call implicitely! One way around it is to define another overload accepting a Func<Task>
as well to redirect async calls there instead!
public class MessagePublisher
{
public static void Publish(Func<Task> action)
{
//...
}
}
9) Flush Streams before Disposing
I found that this one is not known at all in the community. One thinks that disposing things do tear down everything in the most optimal way. But that might not be always the case when you’re using Stream
or StreamWriter
.
When you call the Dispose()
for those, it will synchronously flush the buffer blocking the current thread. If your code is IO heavy, this may result in thread starvation.
// this calls Dispose() under the hood
using (var stream = new StreamWriter(httpResponse))
{
await stream.WriteAsync("Hello World");
}
An easy fix for the above is to call DisposeAsync()
instead or manually call FlushAsync
to do those operations asynchronously.
// this calls DisposeAsync() under the hood
await using (var stream = new StreamWriter(httpResponse))
{
await stream.WriteAsync("Hello World");
}
// this calls DisposeAsync() under the hood
using (var stream = new StreamWriter(httpResponse))
{
await stream.WriteAsync("Hello World");
stream.FlushAsync();
}
10) Do not directly return task in async functions
This one is a bit oppinionated, I have seen both and there’s quite some arguments on both sides. But for the majority of codebases, one should always await underlying calls.
public Task<int> BadUpdateCountersAsync()
=> UpdateCountersAsync();
You may gain a very minor performance benefit if you do the above, by skipping a state machine but I doubt that is even impactful or measurable for most use cases. But you lose a lot in term of debug-ability and exception handling.
The debugger will just hang, Exceptions will entirely skip the context. There’s some actual benefits of having a state machine as those are part of its job.
public async Task<int> GoodUpdateCountersAsync()
=> await UpdateCountersAsync();
Conclusion
By using those conventions, you will generally avoid bad things and bad code leaking away in your application. I tried to compile the most used ones I saw but there’s many more to discuss.
A underrated document is Stephen Toub’s explanation of async/await here. It is a must read if you care for that!