Mutex release throws exception because await changes context

54 Views Asked by At

I have a service that i will inject it as singleton that has:

    private readonly Dictionary<string, List<string>> _fromToAll = [];
    private readonly Mutex _mutex = new();

and I want the dictionary to be lazy initialized only once when needed like:

    private async Task<Dictionary<string, List<string>>> FindFromToAll()
    {
        try
        {
            _mutex.WaitOne();
            if (_fromToAll.Count > 0)
            {
                return _fromToAll;
            }

            foreach (var serviceType in FindAllServiceTypes())
            {
                var service = ActivatorUtilities.CreateInstance(provider, serviceType);
                var fromTo = await service.Invoke<Task<List<string>>>("FromTo");
                _fromToAll.Add(serviceType.Name.Replace("Service", null), fromTo);
            }

            return _fromToAll;
        }
        finally
        {
            _mutex.ReleaseMutex();
        }
    }

but when I invoke and wait for the FromTo Task that returns a list of strings the context changes and when i release the mutex it crashes because i release it from an another thread, the exception message looks like this: "Object synchronization method was called from an unsynchronized block of code."

Any idea why this happens or how can I do it differently? Thank you in advance

Even if i add ConfigureAwait(true) doesn't switch the context to the captured one.

1

There are 1 best solutions below

0
Etienne de Martel On

Mutexes have thread affinity, which means that whenever you lock it on one thread, that same thread has to unlock it. When you await something without a synchronisation context, the continuation goes on the thread pool, and so, odds are very high that the thread running it (and thus the unlock) is different from the one that initially locked it.

The solution is to use a different primitive. The easiest approach is to use SemaphoreSlim, which is async-aware, with an initial count of 1:

private readonly SemaphoreSlim _sem = new SemaphoreSlim(1);

And then, your code becomes:

await _sem.WaitAsync();
try
{        
    // rest of the code is unchanged
}
finally
{
    _sem.Release();
}

Just like Mutex, SemaphoreSlim is disposable, so make sure you dispose of it when you're done.

As an added bonus, you can pass a CancellationToken to WaitAsync() if you want a cancellable wait.