I have written a lot about the conversion of DasBlog Web Forms to ASP.NET Core code, check out some of those posts here:

However, it was not until I started integrating features that would add or delete a blog posts that I realized how much the threading paradigm had changed. In fact I am realizing now that I took for granted the degree to which ASP.NET (as in System.Web) protected me from myself. Let me explain.

The .NET framework includes the concept of a SynchronizationContext, and so most developers who have been doing multithreaded programming have to some degree taken advantage of an architecture that protects them silently. However, with the introduction of ASP.NET Core, there is no SynchronizationContext. So I think it is now more important than ever to understand what the SynchronizationContext did for us and what programming without it now means.

SynchronizationContext in ASP.NET (System.Web)

Asynchronous pages were introduced to ASP.NET is version 2, before this time every request needed a thread until the request completed, which as you might imagine is highly inefficient. With asynchronous pages, the thread handling the request could begin each operation and then return back to the ASP.NET thread pool. At the conclusion of the operation, another thread from the ASP.NET thread pool would be able to complete the request. SynchronizationContext was designed for ASP.NET to manage this process, here are the most important aspects of its work:

  • Provides a way to queue a unit of work to a context
  • Every thread has a "current" context
  • Keeps a count of outstanding asynchronous operations

For this discussion the most noteworthy thing to consider for ASP.NET (System.Web) SynchronizationContext is that it executes "exclusively", put another way, each delegate executes one at a time (but not necessarily in order).

Threading in ASP.NET Core

Whether you knew about these ideas before or not it is important to repeat that ASP.NET Core has no SynchronizationContext! This means that as you port code from, say, a System.Web ASP.NET MVC app to run on Core, it is possible that everything will compile and run fine, however, as soon as a nominal workload is applied you may see classic signs of race conditions or other types of object corruption. Here are the most important ideas to remember when running without SynchronizationContext:

  • Task continuations are queued against the thread pool and can run in parallel
  • HttpContext is not thread safe!
  • No deadlocks if you block a Task with Task.Wait or Task.Result

We can look more closely at what that means for bullet point one, in the following code sample we gather up a list of fifty Tasks and then perform a WhenAll:

public Task GatherListOfScores()
{
    List<int> listOfScores = new List<int>(); 
    var jobs = new Task[50];

    for(int i=0; i<jobs.Length; i++)
    {
        jobs[i] = GetScoreAsync(listOfScores, i);
    }

    return Task.WhenAll(jobs);
}

private async Task GetScoreAsync(List<int> scores, int score)
{
    await Task.Delay(200);
    scores.Add(score);
}

The weakness in this code is that we have a reference to List<int> listOfScores in each Task and they could run in parallel, regardless of this obvious issue, if we were running this code in ASP.NET (System.Web) it would be fine because the SynchronizationContext would queue each continuation, executing them one at a time (exclusively) thereby protecting the List<int> object from corruption.

This is not the case for ASP.NET Core, again there is no queuing, so each task will be scheduled to run on a ThreadPool thread in parallel. This gives each Task the ability to manipulate the List<int> listOfScores concurrently which will eventually cause as an error.

So how do we fix this? We follow more robust threading practices, the easiest option would be to exchange List<int> for something explicitly thread safe like a ConcurrentBag<T> or ConcurrentStack<T>. Alternatively you could simply put a lock in the GetStoreAsync method as follows:

private async Task GetScoreAsync(List<int> scores, int score)
{
    await Task.Delay(200);

    lock(scores)
    {
        scores.Add(score);
    }
}

I must admit this issue took me by surprise in DasBlog it just started breaking inexplicably, the fact is SynchronizationContext is excellent at what it does and you tend not to think about it. However, ASP.NET Core is built for speed and so we are necessarily a little closer to the metal and it is incumbent upon us to know how our applications will react on this new platform. For more information on this there is a great presentation from the ASP.NET Core team at a recent NDC Conference.