19

As the famous blog post from Stephen Cleary dictates, one should never try to run async code synchronously (e.g. via Task.RunSynchronously() or accessing Task.Result). On the other hand, you can't use async/await inside lock statement.

My use case is ASP.NET Core app, which uses IMemoryCache to cache some data. Now when the data is not available, (e.g. cache is dropped) I have to repopulate it, and that should be guarded with lock.

public TItem Get<TItem>(object key, Func<TItem> factory)
{
    if (!_memoryCache.TryGetValue(key, out TItem value))
    {
        lock (_locker)
        {
            if (!_memoryCache.TryGetValue(key, out value))
            {
                value = factory();
                Set(key, value);
            }
        }
    }
    return value;
}

In this example, the factory function can not be async! What should be done if it has to be async?

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Titan
  • 2,875
  • 5
  • 23
  • 34
  • 3
    https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/ – Lasse V. Karlsen May 30 '17 at 18:35
  • 1
    [`SemaphoreSlim.WaitAsync`](https://msdn.microsoft.com/en-us/library/hh462805(v=vs.110).aspx) is your friend. – spender May 30 '17 at 18:47
  • Looking at your question from higher perspective, I think you may be overlooking possibility of not having to lock at all, and take advantage of `Task`s themselves to do synchronization for you. I was typing up the answer in that direction when the question got closed :( – LB2 May 30 '17 at 18:49
  • 2
    This question deserves discussion. Just that there is a similar question with an answer, does not really mean that the problem is definitely resolved and solution is clear. Please reopen the question. – Titan May 30 '17 at 18:54
  • Did you read the link in the first comment? Seems to answer your questions. – Evk May 31 '17 at 05:56

1 Answers1

24

An easy way to coordinate asynchronous access to a shared variable is to use a SemaphoreSlim. You call WaitAsync to begin an asynchronous lock, and Release to end it.

E.g.

private static readonly SemaphoreSlim _cachedCustomersAsyncLock = new SemaphoreSlim(1, 1);
private static ICollection<Customer> _cachedCustomers;

private async Task<ICollection<Customer>> GetCustomers()
{
    if (_cachedCustomers is null)
    {
        await _cachedCustomersAsyncLock.WaitAsync();

        try
        {
            if (_cachedCustomers is null)
            {
                _cachedCustomers = GetCustomersFromDatabase();
            }
        }
        finally
        {
            _cachedCustomersAsyncLock.Release();
        }
    }

    return _cachedCustomers;
}
C. Augusto Proiete
  • 24,684
  • 2
  • 63
  • 91
  • _cachedCustomersAsyncLock is null == false you mean – cubesnyc Jul 11 '20 at 00:41
  • 1
    @cubesnyc Actually I mean `if (_cachedCustomers is null)`. I'm using the [double-checked locking](http://en.wikipedia.org/wiki/Double-checked_locking) pattern and the purpose is to avoid the expensive lock operation which is only going to be needed once (when the `GetCustomers` is first accessed) – C. Augusto Proiete Jul 12 '20 at 00:17
  • There is a bug in your code. If the `_cachedCustomers` is `null` before the `await`, and not `null` after the `await`, the semaphore will be acquired and not released. – Theodor Zoulias Jul 12 '20 at 03:07
  • 1
    @TheodorZoulias Good catch! Thanks. I've updated the answer – C. Augusto Proiete Jul 12 '20 at 15:26
  • Now it's OK. :-) Btw the double-checked locking pattern is [tricky](https://stackoverflow.com/questions/1964731/the-need-for-volatile-modifier-in-double-checked-locking-in-net). You risk receiving partially initialized objects if you don't use `volatile`, or `Volatile.Read`, or `Thread.MemoryBarrier`. – Theodor Zoulias Jul 12 '20 at 15:48