Async/await - Pitfall 2 - Synchronisation

In keeping with my previous post in this series, this one is about context synchronisation in .net web applications.

There is no SynchronisationContext in .net core (.net 5+). Keep that in mind.

When you await a task in the .net framework, once the task is complete, the default is to resume execution using the same context. In asp.net, this is a request context, like HttpContext.

By calling .ContinueAwait(false) on a task, what you're doing is telling the CLR to execute your code without resuming the original context. Or more accurately, if continuation is set to false, the runtime bypasses queuing the continuation with the current context. In a web app, we should strive not to rely on context, and let the runtime optimise execution as much as possible.

What do we gain?

If we don't need to resume context, we can scale out, letting any processing thread take on the continuation, the benefit here similarly equate to that of the benefits of statelessness. The less context (state) we rely on in a given request, the more optimisations we can make; we can scale out horizontally much easier. This is true with application threads as it is with applications themselves. We get simplicity, issues are much easier to identify if we don't require requests to be in certain states before problems surface. Testing is another benefit we naturally get when we don't relying on building up large contexts.

Context isn't state of course, but in the case of ConfigureAwait and the SynchronizationContext it does reflect the benefits of not requiring context quite well.

If you don't add ConfigureAwait(false) when using async/await, we lose a large part of the why we would want to have an asynchronous flow in the first place. Yes, it would still be non-blocking, but would limit the additional throughput capacity we gain if we did add it.

Capture what you need from the context

If you can, instead of resuming with the original context, see if you can capture a reference to what you need from it before losing it.

In a web app, the context usually refers to a request. In asp.net, this is HttpContext.Current.

public async Task<string> BadAccess()
{
    await Task.Delay(10).ConfigureAwait(false);
    return System.Web.HttpContext.Current.Request.Url.AbsoluteUri;
    
    -- NullReferenceException because we've lost HttpContext.Current
}

// Capture what you need prior to losing the context.
public async Task<string> GoodAccess()
{
    var request = System.Web.HttpContext.Current.Request;
    await Task.Delay(10).ConfigureAwait(false);
    return request.Url.AbsoluteUri;
    
    -- Good
}

We don't actually need to do this in .net controllers as the base controller captures the HttpContext and exposes Request/Response objects directly, but it serves as a good example!

Best practice in libraries

Even though .net core (and v5+) has no synchronisation context, if you're building a library which is .net standard and can be used in the .net framework, you should still add .ConfigureAwait(false) where you normally would to show your intent and provide better support.

Further reading on SynchronizationContext

https://docs.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext