When using async
/await
, you’ll want to use async Task
methods most of the time, and use async void
methods only for event handlers (see “Async/Await – Best Practices in Asynchronous Programming“, by Stephen Cleary, MSDN Magazine, March 2013).
This conventional wisdom works great if you’re building something like a WPF (GUI) application, and event handlers are invoked occasionally as a result of user interaction (e.g. user presses a button, and an event fires). However, there is another class of event handlers that are invoked as part of a dispatcher loop in a third-party library. async void
can be pretty dangerous in these cases.
async void Event Handlers in RabbitMQ
Let’s take the example of RabbitMQ. We’ll set up a basic publisher and consumer. A fundamental property of message queues is that messages are delivered in order, and that’s what we expect to happen.
First, install the RabbitMQ Client library via NuGet:
Install-Package RabbitMQ.Client
Then, we can set up a basic publisher and consumer:
static void Main(string[] args) { Console.Title = "async RabbitMQ"; var factory = new ConnectionFactory(); using (var connection = factory.CreateConnection()) using (var channel = connection.CreateModel()) { const string queueName = "testqueue"; // create queue if not already there channel.QueueDeclare(queueName, true, false, false, null); // publish var props = channel.CreateBasicProperties(); for (int i = 0; i < 5; i++) { var msgBytes = Encoding.UTF8.GetBytes("Message " + i); channel.BasicPublish("", queueName, props, msgBytes); } // set up consumer var consumer = new EventingBasicConsumer(channel); consumer.Received += Consumer_Received; channel.BasicConsume("testqueue", true, consumer); Console.ReadLine(); } }
Our consumer will call the Consumer_Received
event handler whenever a message is received. This is the first version of the event handler:
private static void Consumer_Received(object sender, BasicDeliverEventArgs e) { var body = e.Body; var content = Encoding.UTF8.GetString(body); Console.WriteLine("Began handling " + content); Thread.Sleep(1000); Console.WriteLine("Finished handling " + content); }
If we run this now, the messages are processed one at a time and in order just as we expect:
Now, let’s change the event handler to an asynchronous one:
private static async void Consumer_Received(object sender, BasicDeliverEventArgs e) { var body = e.Body; var content = Encoding.UTF8.GetString(body); Console.WriteLine("Began handling " + content); await Task.Delay(1000); Console.WriteLine("Finished handling " + content); }
If we run this now…
…we see that our concurrency and ordering guarantees have just gone out the window.
Understanding async void, Revisited
In my recent article, “In-Depth Async in Akka .NET: Why We Need PipeTo()“, I explained what happens when you call async void
methods. Let’s recap that.
Say we have this program. We’re calling an async void
method in a loop.
static void Main(string[] args) { Console.Title = "async void"; for (int i = 0; i < 5; i++) RunJob("Job " + i); Console.ReadLine(); } static async void RunJob(string str) { Console.WriteLine("Start " + str); await Task.Delay(1000); Console.WriteLine("End " + str); }
When you call an async void
method, it’s done in a fire-and-forget manner. The caller has no way of knowing whether or when the operation ended, so it just resumes execution immediately, rather than waiting for the async void
method to finish. So you end up with parallel and interleaved execution such as this:
If we change RunJob()
to be synchronous instead…
static void RunJob(string str) { Console.WriteLine("Start " + str); Thread.Sleep(1000); Console.WriteLine("End " + str); }
…you’ll see that everything happens one at a time and in order:
So you have to be really careful when using async void
:
- There is no way for the caller to await completion of the method.
- As a result of this,
async void
calls are fire-and-forget. - Thus it follows that
async void
methods (including event handlers) will execute in parallel if called in a loop. - Exceptions can cause the application to crash (see the aforementioned article by Stephen Cleary for more on this).
Fixing async void event handlers
Despite these problems, if you want to await
in your event handler, you have to make it async void
. To prevent parallel and interleaved execution, you have to lock. However, you can’t await
in a lock
block, so you need to use a different synchronisation mechanism such as a semaphore.
My own Dandago.Utilities provides a ScopedAsyncLock
that allows you to neatly wrap the critical section in a using
block:
private static ScopedAsyncLockFactory factory = new ScopedAsyncLockFactory(); private static async void Consumer_Received(object sender, BasicDeliverEventArgs e) { using (var scopedLock = await factory.CreateLockAsync()) { var body = e.Body; var content = Encoding.UTF8.GetString(body); Console.WriteLine("Began handling " + content); await Task.Delay(1000); Console.WriteLine("Finished handling " + content); } }
Like this, messages are consumed one at a time, and in order:
ScopedAsyncLockFactory
uses a semaphore underneath, so don’t forget to dispose it!
Rather than having to synchronise your task based event handler, if queued messages must be handled sequentially, wouldn’t it just be better to use an ordinary synchronous event handler?
Yes, if you can! But there are times when you need to use async-only APIs, such as Microsoft’s HttpClient. In such cases, you absolutely need to have asynchronous event handlers.
Can’t you call an asynchronous method synchronously when you need to which would negate the concern of “when you can”?
In a real scenario Yes, but he/she is trying to make a point and this is the way he/she choose.
Best Regards
You can
Wait()
or call.Result
, but then you’d block the thread, losing the benefits of asynchronous calls altogether. Also using both synchronous waiting andawait
together is a dangerous practice leading to deadlocks.Good post indeed. Just wondering why you crafted your own asyncLock when this (semi well from the bighouse anyway) one exists : https://blogs.msdn.microsoft.com/pfxteam/2012/02/12/building-async-coordination-primitives-part-6-asynclock/
Stephen Toub is one of THE ASYNC guys
I would just be curious how you see yours being better/different
Hi, I was simply not aware of that, but I wrote this little library to provide people with an easy package that they can install and use without having to reimplement the same code every time.
Unfortunately I don’t have time at this stage to revisit what I did almost 2 years ago, understand in depth what Stephen Toub did, and draw a comparison. However if you find any alternative solution that works well, then by all means go for it.
This maybe helpful for others in the future: the RabbitMQ NuGet package now also offers a consumer which is able to handle messages in an async manner: AsyncEventingBasicConsumer
For more info, check out the .NET/C# Client API guide: https://www.rabbitmq.com/dotnet-api-guide.html#consuming-async
That’s right, in fact I wrote about it here: https://gigi.nullneuron.net/gigilabs/asynchronous-rabbitmq-consumers-in-net/