Do AsyncLocal<> values need to be thread-safe?

Viewed 1006

A question came up in a recent PR review about whether or not an AsyncLocal<IDictionary<>> should use ConcurrentDictionary<>. My thinking is that it does not need to because an AsyncLocal value won't be accessed by multiple threads at the same time. But I want to be certain.

Do we need to worry about thread-safety of objects kept in AsyncLocal? Please explain why or why not. Bonus points for a unit test demonstrating the asserted answer.

1 Answers

AsyncLocal is available to all threads in an asynchronous control flow.

Let's take this example:

using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp15
{
    class Program
    {

        static AsyncLocal<string> _asyncLocalString = new AsyncLocal<string>();

        static async Task Main(string[] args)
        {
            _asyncLocalString.Value = "TestString";

            var tasks = Enumerable.Range(0, 10).Select(_ => GetString());
            var results = await Task.WhenAll(tasks);
        }

        private static async Task<string> GetString()
        {
            await Task.Yield();
            return _asyncLocalString.Value + Thread.CurrentThread.ManagedThreadId;
        }
    }
}

Here multiple threads can access the value "TestString" at the same time.

enter image description here

Additionally, once the value is pulled out of Async local it needs to be treated just like any other reference. If it is used in callbacks, returned to the caller, captured in a closure etc, it can be exposed to other threads. With reference types, values could be modified externally and race conditions could occur.

Update: Here is an example with a dictionary:

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp15
{
    class Program
    {

        static AsyncLocal<Dictionary<string, int>> _asyncLocalDict = new AsyncLocal<Dictionary<string, int>>();

        static async Task Main(string[] args)
        {
            _asyncLocalDict.Value = new Dictionary<string, int>();

            var tasks = Enumerable.Range(0, 10).Select(_ => Race());
            await Task.WhenAll(tasks);
        }

        private static async Task Race()
        {
            await Task.Yield();
            var dict = _asyncLocalDict.Value;
            if (!dict.ContainsKey("race")) dict["race"] = 0;
            dict["race"]++;
        }
    }
}

enter image description here

Related