Skip to main content

Async/Await: It’s time!

Async/Await is a language feature introduced in C# 5.0 together with Visual Studio 2012 and the .NET 4.5 runtime. With Visual Studio 2015 almost ready to be shipped to end-users, we can see that async/await has been around for quite some time now. Yet NServiceBus hasn’t been exposing asynchronous APIs to its users. Why the await?

We have been carefully observing the adoption of async/await in the market and weighing its benefits against its perceived complexity. Over the years async/await has matured and proven its worth. Now the time has come to discuss our plans regarding the async/await adoption in NServiceBus and our platform.

TL;DR

  • Future versions of the NServiceBus API will be async only.
  • Synchronous versions of the API will be removed in future versions.
  • Microsoft and the community have made this decision for you, by moving toward making all APIs which involve IO operations async-only.
  • Don’t panic! We will help you through this difficult time.

You’re going to be blown away by the additional benefits that asynchronous NServiceBus APIs will provide. But first, let’s review why asynchronous APIs are such a big deal in the first place.

🔗Stay responsive

Asynchronous programming is essential for activities that are blocking in nature. Common examples of blocking operations include web calls, working with files, working with images, and more. In a synchronous programming model, a call to an HTTP endpoint would cause the calling thread to be blocked until the call completes. In an asynchronous model, the calling thread can continue doing other work that doesn’t depend on the HTTP result until the blocking task has finished. This enables the thread-scheduling infrastructure to efficiently manage the operating system’s resources.

In summary, asynchronous programming can avoid performance bottlenecks and improve the responsiveness of your system everywhere where I/O operations are involved.

🔗Not just “A .NET thing”

Async/await is, like LINQ, taking over the world. When Microsoft first announced better language support for asynchronous operations, other languages started to adopt it by putting specifications into place. Here are a few examples:

🔗The die is cast - Alea iacta est

Microsoft and the community made the decision for you. For example, consider Azure DocumentDB which was released on 8th April 2015 as Generally Available in Azure. The .NET client library provided by the Azure SDK with the name Microsoft.Azure.DocumentDB is async only where IO operations are involved. The main connection point to the Azure DocumentDB service is the DocumentClient which acts as a client-side logical representation of the Azure DocumentDB service. The client is used to configure and execute requests against the service and therefore heavily IO bound, by leveraging the HTTP APIs of Azure. All queries issued against the DocumentDB service are abstracted by IDocumentQuery, and everything there is async as well. The Azure SDK as a whole is moving towards async only, sync versions are either deprecated or (sometimes even worse) only the asynchronous versions get improved.

🔗But isn’t messaging already async?

One way how NServiceBus allows you to control message throughput is by setting the MaximumConcurrencyLevel option.

The MaximumConcurrencyLevel limits the maximum number of message consuming threads that NServiceBus will allocate. Let us assume we set the MaximumConcurrencyLevel to eight. In this case, NServiceBus will use a maximum of eight message consuming threads from the thread pool.

This is like building an eight-lane highway. There may be no cars on the road, or one, or two, but the number of lanes is fixed and at no time can there be any more than eight cars. So at any given time we can process a maximum of eight messages in parallel. Strictly speaking, we currently process messages in parallel, but not asynchronously.

Asynchronous means non-blocking, but during the consumption of those eight messages in parallel, currently each thread (and all of its allocated resources) is blocked until that message is consumed.

If we could combine parallelism with non-blocking, asynchronous execution, we could free up that message processing thread to do other work while the asynchronous operation is happening. This is where async/await comes into play.

In the case of those eight message consumption threads, every time a handler calls into an I/O bound resource (e.g. a database, web service, or the underlying transport) then that thread could go process other messages until the I/O operation completes, therefore allowing NServiceBus to more efficiently use your datacenter’s resources.

You might not be that worried about better resource usage in your datacenter, but if you are running in the cloud, using resources more efficiently means potentially saving a lot of money.

🔗Asynchronous send/publish

The bus API provided by NServiceBus has methods like Send and Publish. In the future there will only be asynchronous versions of those methods available. Let’s see what this could look like:

// Somewhere in your application code
await bus.SendAsync(...);
await bus.PublishAsync(...);

Imagine the capabilities those APIs will provide. For example, when you send a message from a WPF or Windows Forms application, the UI thread won’t be blocked. The same will be true when sending or publishing from IIS worker threads in an MVC, WebAPI, or WCF application.

The code above is doing a sequential send and then a publish. Now what if you want to do both at the same time? With async/await this is simple to achieve.

// Somewhere in your application code
var send = bus.SendAsync(new Message());
var publish = bus.PublishAsync(new Message());
await Task.WhenAll(send, publish);

Or multiple sends?

var sends = new List<Task>();
foreach(var data in businessData)
{
	sends.Add(bus.SendAsync(new Message()));
}
await Task.WhenAll(sends);

Note: The APIs above are only illustrative examples and don’t necessarily reflect the APIs we will be shipping.

🔗Asynchronous receive

When you are receiving a message you are most likely doing quite a bit more: storing something in a database, publishing messages back onto the bus, etc. Here is a somewhat contrived example depicting what the interaction between a message handler and Entity Framework 6 and higher could potentially look like:

public class BlogpostMesageHandler : IHandleMessagesAsync<CreateNewBlogpost>
{
	public IBus Bus { get; set; }

	public async Task HandleAsync(CreateNewBlogpost message)
	{
        using (var db = new BloggingContext()) 
        { 
            db.Blogposts.Add(new Blogpost 
            { 
                Title = message.Title
            }); 

            await db.SaveChangesAsync(); 

            var posts = await (from p in db.Blogposts 
                        orderby p.Name 
                        select p).ToListAsync(); 

			var publishes = new List<Task>();
            foreach (var post in posts) 
            { 
                publishes.Add(Bus.PublishAsync(new BlogpostPublished(post.Title))); 
            } 

			await Task.WhenAll(publishes);
        } 
	}
}

Note: The APIs above are only illustrative examples and don’t necessarily reflect the APIs we will be shipping.

🔗Where are my synchronous APIs?

The synchronous versions of the APIs directly involved in the messaging handling and message sending pipeline will be removed in upcoming versions of NServiceBus. By “removed” we mean there will be no backwards compatible side-by-side version available.

We realize that this is breaking from our usual backwards-compatible everything approach, but in this case, since invoking synchronous code in an asynchronous API is really simple, there is no need for a synchronous handler. Let us show you an example:

public class YourMessageHandler : IHandleMessagesAsync<YourMessage>
{
	public Task HandleAsync(YourMessage message)
	{
		// your synchronous code
		return Task.FromResult(true);
	}
}

Note: The API above is only an illustrative example and doesn’t necessarily reflect the APIs we will be shipping.

You might be wondering why we are only exposing asynchronous APIs for future versions. There are a few reasons which are rooted in the best practices around async and await:

  • Prefer async Task methods over async void methods
  • Don’t mix blocking and async code
  • Use ConfigureAwait(false) when you can

Here is an example. Assume you are using the bus in a UI application

// Application event handler calls synchronous Send
private void button1_Click(object sender, EventArgs e) 
{ 
    Send(...); 
} 

// Synchronous API that wraps Async API
private void Send(...) 
{ 
    SendAsync(...).Wait(); 
} 

// Actual implementation is async
private async Task SendAsync(...) 
{ 
    await bus.SendAsync(...); 
}

It may look like having a synchronous wrapper to the asynchronous implementation would make everything easy. Unfortunately, this is actually a really bad idea. As explained by the Parallel Programming with .NET blog (text changed only slightly to fit our use case):

While this may look innocent, invoking Send from the UI thread like this is almost certainly going to deadlock. By default, await’ing a Task will post the remainder of the method’s invocation back to the SynchronizationContext from which the await was issued (even if that “remainder” is just the completing of the method). Here, the UI thread calls Send, which calls SendAsync, which awaits a Task that won’t complete for a few milliseconds. The UI thread then synchronously blocks in the call to Wait(), waiting for the Task returned from SendAsync to complete. A few milliseconds later, the task returned from bus.SendAsync completes, and the await causes the remainder of SendAsync’s execution to be posted back to the UI’s SynchronizationContext. That continuation needs to execute so that the Task returned from SendAsync may complete, but that continuation can’t execute until the call to button1_Click returns, which won’t happen until the Task returned from SendAsync completes, which won’t happen until the continuation executes. Deadlock!

Had Send instead been invoked from a console app, or from a unit test harness that didn’t have a similarly constrictive SynchronizationContext, everything likely would have completed successfully. This highlights the danger in such sync over async behavior: its success can be significantly impacted by the environment in which it’s used. That’s a core reason why it should be left up to you dear user to decide whether to do such blocking, as you are much more aware of the environment in which you’re operating than [the NServiceBus code] you’re calling.

So clearly, it would be irresponsible to simply wrap an asynchronous API with a synchronous one. And going the other way, wrapping our current synchronous API with an asynchronous one, would be worse; it doesn’t really give you the real advantages of asynchronous programming.

🔗You moved my cheese!

All async/await benefits aside, we understand that making these kinds of changes across your code base will be no small task. That’s why we’re working on a migration guide to make the process as smooth and painless as possible. This guide will include samples for the various asynchronous and synchronous business scenarios out there - and if you need more help, our support team will be happy to help you work through it.

We truly believe that the change to a full async API is really worth it, because taking the plunge and fully adopting async/await is going to take your systems to the next level in terms of scalability and flexibility.

🔗In closing

We will continue doing our best to evolve our already solid platform and tooling to the ever-changing technology landscape so that you can stay focused on solving your business problems. We embrace change whether it be in technology or the .NET ecosystem if the benefits are really worth it. You trust us to provide you with the tools and experience to help you make your project succeed, and we take that responsibility very seriously. The time has come to adopt async/await.

🔗Additional reading


About the author: Daniel Marbach is a solution architect at Particular Software, Microsoft Integration MVP, coach and passionate blogger.

Share on Twitter
Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.