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!