Vaughan Reid's blog

ASP.NET Core doesn't use a SynchronizationContext

Yesterday I was trying to show how to diagnose a deadlock situation when using blocking code over async. This was a pretty common mistake in ASP.NET and a nice scenario to add to my measurement series. I created a controller like this in my ASP.NET Core sample application and strangely it worked without error.


[HttpGet]
public IActionResult Double(int num)
{
	var resultTask = GetDouble(num);
	resultTask.Wait();
	return Ok(resultTask.Result);
}

private async Task<int> GetDouble(int num)
{
	await Task.Delay(TimeSpan.FromSeconds(1);
	return num * 2;
}

Firstly and most importantly: Please don’t do this even though it now works. By making async calls synchronous you miss all the performance gains of using async. ASP.NET core is fully async so there very little reason you should need to do this.

It turns out that I’m 3 years out of date because this was how it worked from the start of dotnet core.

Previously in classic ASP.NET, a controller handler would use a AspNetSynchronizationContext which kept the main execution in a single thread. Running async code synchronously would cause a deadlock. The code awaiting the async call will hold the context that the async call wanted to release. This is still a problem in dotnet applications with UI threads by the way.

But now async calls use a thread pool context instead. This means that this deadlock doesn’t occur because there are different threads releasing and awaiting.

One small side effect of this is that it is technically a breaking change. There is now implicit parallelism when using async calls that wasn’t there before.

Here is an admittedly strange example that could break in ASP.NET Core but would work in classic ASP.NET.


[HttpGet]
public async  Task<IActionResult> Process()
{
	List<int> allDoubles = new List<int>();
	var task1 =  GetDoubleAndSave(2, allDoubles);
	var task2 = GetDoubleAndSave(7, allDoubles);

	await Task.WhenAll(task1, task2);
	return Ok(string.Join(",", allDoubles));
}
        
private async Task GetDoubleAndSave(int num, List<int> results)
{
	await Task.Delay(TimeSpan.FromSeconds(1));
	results.Add(num * 2);
}

The reason for this is that the Add method in a List isn’t thread safe and each time the code comes out of an await it will run on a separate thread. Its then possible that two different threads will call the add method at the same time. I actually ran this method a few times and got one of these results each time.

4,14
14,4
14
4

As you can see, the last two results happened when two threads tried to Add at the same time.