Asynchronous locking based on a key
Asked Answered
S

5

47

I'm attempting to figure out an issue that has been raised with my ImageProcessor library here where I am getting intermittent file access errors when adding items to the cache.

System.IO.IOException: The process cannot access the file 'D:\home\site\wwwroot\app_data\cache\0\6\5\f\2\7\065f27fc2c8e843443d210a1e84d1ea28bbab6c4.webp' because it is being used by another process.

I wrote a class designed to perform an asynchronous lock based upon a key generated by a hashed url but it seems I have missed something in the implementation.

My locking class

public sealed class AsyncDuplicateLock
{
    /// <summary>
    /// The collection of semaphore slims.
    /// </summary>
    private static readonly ConcurrentDictionary<object, SemaphoreSlim> SemaphoreSlims
                            = new ConcurrentDictionary<object, SemaphoreSlim>();

    /// <summary>
    /// Locks against the given key.
    /// </summary>
    /// <param name="key">
    /// The key that identifies the current object.
    /// </param>
    /// <returns>
    /// The disposable <see cref="Task"/>.
    /// </returns>
    public IDisposable Lock(object key)
    {
        DisposableScope releaser = new DisposableScope(
        key,
        s =>
        {
            SemaphoreSlim locker;
            if (SemaphoreSlims.TryRemove(s, out locker))
            {
                locker.Release();
                locker.Dispose();
            }
        });

        SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(key, new SemaphoreSlim(1, 1));
        semaphore.Wait();
        return releaser;
    }

    /// <summary>
    /// Asynchronously locks against the given key.
    /// </summary>
    /// <param name="key">
    /// The key that identifies the current object.
    /// </param>
    /// <returns>
    /// The disposable <see cref="Task"/>.
    /// </returns>
    public Task<IDisposable> LockAsync(object key)
    {
        DisposableScope releaser = new DisposableScope(
        key,
        s =>
        {
            SemaphoreSlim locker;
            if (SemaphoreSlims.TryRemove(s, out locker))
            {
                locker.Release();
                locker.Dispose();
            }
        });

        Task<IDisposable> releaserTask = Task.FromResult(releaser as IDisposable);
        SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(key, new SemaphoreSlim(1, 1));

        Task waitTask = semaphore.WaitAsync();

        return waitTask.IsCompleted
                   ? releaserTask
                   : waitTask.ContinueWith(
                       (_, r) => (IDisposable)r,
                       releaser,
                       CancellationToken.None,
                       TaskContinuationOptions.ExecuteSynchronously,
                       TaskScheduler.Default);
    }

    /// <summary>
    /// The disposable scope.
    /// </summary>
    private sealed class DisposableScope : IDisposable
    {
        /// <summary>
        /// The key
        /// </summary>
        private readonly object key;

        /// <summary>
        /// The close scope action.
        /// </summary>
        private readonly Action<object> closeScopeAction;

        /// <summary>
        /// Initializes a new instance of the <see cref="DisposableScope"/> class.
        /// </summary>
        /// <param name="key">
        /// The key.
        /// </param>
        /// <param name="closeScopeAction">
        /// The close scope action.
        /// </param>
        public DisposableScope(object key, Action<object> closeScopeAction)
        {
            this.key = key;
            this.closeScopeAction = closeScopeAction;
        }

        /// <summary>
        /// Disposes the scope.
        /// </summary>
        public void Dispose()
        {
            this.closeScopeAction(this.key);
        }
    }
}

Usage - within a HttpModule

private readonly AsyncDuplicateLock locker = new AsyncDuplicateLock();

using (await this.locker.LockAsync(cachedPath))
{
    // Process and save a cached image.
}

Can anyone spot where I have gone wrong? I'm worried that I am misunderstanding something fundamental.

The full source for the library is stored on Github here

Stairwell answered 30/6, 2015 at 12:20 Comment(4)
does this library supports ResizeAsync or in general xxxAsync ? I want to use your library asynchronouslyDeutsch
Wrap the methods in a Task. There's no native Async methods in there as creating threads is expensive.Stairwell
James i was talking about io operation such as saving to a streamDeutsch
Sorry, that'll be a no still I'm afraid. The underlying code Image.Save(stream) has no async overloads.Stairwell
S
80

As the other answerer noted, the original code is removing the SemaphoreSlim from the ConcurrentDictionary before it releases the semaphore. So, you've got too much semaphore churn going on - they're being removed from the dictionary when they could still be in use (not acquired, but already retrieved from the dictionary).

The problem with this kind of "mapping lock" is that it's difficult to know when the semaphore is no longer necessary. One option is to never dispose the semaphores at all; that's the easy solution, but may not be acceptable in your scenario. Another option - if the semaphores are actually related to object instances and not values (like strings) - is to attach them using ephemerons; however, I believe this option would also not be acceptable in your scenario.

So, we do it the hard way. :)

There are a few different approaches that would work. I think it makes sense to approach it from a reference-counting perspective (reference-counting each semaphore in the dictionary). Also, we want to make the decrement-count-and-remove operation atomic, so I just use a single lock (making the concurrent dictionary superfluous):

public sealed class AsyncDuplicateLock
{
  private sealed class RefCounted<T>
  {
    public RefCounted(T value)
    {
      RefCount = 1;
      Value = value;
    }

    public int RefCount { get; set; }
    public T Value { get; private set; }
  }

  private static readonly Dictionary<object, RefCounted<SemaphoreSlim>> SemaphoreSlims
                        = new Dictionary<object, RefCounted<SemaphoreSlim>>();

  private SemaphoreSlim GetOrCreate(object key)
  {
    RefCounted<SemaphoreSlim> item;
    lock (SemaphoreSlims)
    {
      if (SemaphoreSlims.TryGetValue(key, out item))
      {
        ++item.RefCount;
      }
      else
      {
        item = new RefCounted<SemaphoreSlim>(new SemaphoreSlim(1, 1));
        SemaphoreSlims[key] = item;
      }
    }
    return item.Value;
  }

  public IDisposable Lock(object key)
  {
    GetOrCreate(key).Wait();
    return new Releaser { Key = key };
  }

  public async Task<IDisposable> LockAsync(object key)
  {
    await GetOrCreate(key).WaitAsync().ConfigureAwait(false);
    return new Releaser { Key = key };
  }

  private sealed class Releaser : IDisposable
  {
    public object Key { get; set; }

    public void Dispose()
    {
      RefCounted<SemaphoreSlim> item;
      lock (SemaphoreSlims)
      {
        item = SemaphoreSlims[Key];
        --item.RefCount;
        if (item.RefCount == 0)
          SemaphoreSlims.Remove(Key);
      }
      item.Value.Release();
    }
  }
}
Steeve answered 2/7, 2015 at 21:6 Comment(26)
"is to attach them using ephemerons", You kind of lost me there, can you explain what you meant by that?Commutator
Ephemerons are a dynamic language concept that ties one object to the lifetime of another. Like the properties you can add to ExpandoObject, but ephemerons can be attached to any object (more like JavaScript properties in that regard). The only .NET ephemeron is ConditionalWeakTable, a difficult to use object. I wrote a simple wrapper library called ConnectedProperties.Steeve
This is brilliant! An elegant approach that I would have forever over-engineered in attempting. I'd gotten a never disposing implementation working but really wasn't happy with the ever increasing memory usage. Very much appreciated!Stairwell
How would you adapt the solution to prevent locking for mostly read use cases?Shemikashemite
@too: I wouldn't. The time spent under lock is extremely short, so even if most uses are reads, it probably would not be worth changing.Steeve
Thanks! BTW, why didn't you make AsyncDuplicateLock class static?Parabasis
@OlegasGončarovas: I prefer static instances over static classes, when reasonable.Steeve
Anyone has implemented this with a more unit testable implementation?Brotherton
SemaphoreSlim has a built-in CurrentCount property so you don't need to refcount. Also, we can use ConcurrentDictionary to get rid of the locks. Otherwise great solution.Aposematic
@StephenCleary, I'm trying to update your solution so that I can allow multiple threads access to a resource, but at the same time the threads that are blocked (till Release is called so that the next thread can enter) to just skip or exit instead of waiting. Do you have any suggestions that I can use?Canaille
@boris: I almost never use "try locks"; to me, this indicates there may be a mismatch somewhere. So the first recommendation I have is to step back and look at the bigger picture, and see if a completely different approach would make better sense. That said, you can do "try locks" with SemaphoreSlim - call WaitAsync(0). The trickier part is what API you're going to expose. A nullable IDisposable is one option (null meaning the lock wasn't taken).Steeve
I've been reading about always locking using a dedicated object, but in this example you seem to lock on the SemaphoreSlims dictionary instance. Is it because it's readonly? But it's also static? Would dedicated lock object instance do any good here?Uraninite
@kor_: Locking using a dedicated object is never a bad idea. In this specific case, I can lock on the dictionary instance instead because it's private and never exposed. Since this code here is the only code that can access the dictionary instance, I know that nothing else can ever lock on it.Steeve
@AlexfromJitbit No the CurrentCount() does not work, because it does NOT say how many are waiting to get into the Semaphore but how many are currently processed by the Semaphore.Syblesybley
@StephenCleary I don't understand why there's a "LockAsync" method? "Lock" seems to provide the same functionality, and I can't see what advantage or where you'd use "LockAsync" instead. Am I missing something?Calyces
@DanielJamesBryars: LockAsync is asynchronous, so it doesn't block the thread while acquiring the semaphore.Steeve
Based on this solution, I created a .NET Standard 2.0 library available on NuGet nuget.org/packages/AsyncKeyedLock and GitHub github.com/MarkCiliaVincenti/AsyncKeyedLockLaurent
I am not sure if keeping SemaphoreSlims dictionary field as static is good idea. In an application when someone might keep multiple instances of AsyncDuplicateLock completely independent keys can cause collisions. Why not keep it a regular instance variable and let client handle AsyncDuplicateLock lifetime (for example keeping multiple separate instances of AsyncDuplicateLock static)?Cenis
@LadislavBohm: The op's question used a static readonly dictionary, so my answer mirrored that.Steeve
I stumbled upon this question, after implementing a very similar solution myself: github.com/amoerie/keyed-semaphores The main index of semaphores is based on ConcurrentDictionary, but the ref counting happens under a lock, where the lock is the semaphore itself. This avoids lock contention until a lot of requests for the same key occur. I'd be interested to hear feedback!Monacid
@Cenis I updated the library at nuget.org/packages/AsyncKeyedLock / github.com/MarkCiliaVincenti/AsyncKeyedLock to not use static. An instance can also be injected.Laurent
Would adding a cancellation token be okay here?Hiram
@Groo: 1) No; there is no finalizer, and it would be wrong for a finalizer to call Dispose() anyway. 2) You can create the instance then if you want, but it still won't decrement the counter in case of an exception; you'd need something like a CER to make this safe in the face of rare fatal exceptions (like memory corruption) - or you can ignore fatal exceptions and expect them to take down the process, just like 99% of the code in the world.Steeve
@StephenCleary: sorry re point 2, what I meant was, WaitAsync can throw when the thread is aborted (arguably not the normal scenario, but it's not a fatal exception), or if you use the CancellationToken overload in which case OperationCanceledException is an expected behavior. In that case, the counter will remain incremented forever. So since the only code that decrements the counter for this key is in the Release.Dispose method, that's what I said it would make sense to create it and then dispose it in case of an exception.Hinayana
Regarding point 1, yes also sorry for my rambling, of course it won't be called twice unless the finalizer is explicitly implemented to call it, or there is a coding error that results in it being called twice, but I understand this to be a part of the implementation requirement, according to MSDN: If an object's Dispose method is called more than once, the object must ignore all calls after the first one. The object must not throw an exception if its Dispose method is called multiple times.Hinayana
@Groo: If you need to allow multiple disposals, you can use Disposables. Regarding thread aborts, dangling locks are one of the possible results of thread aborts, which is why they're strongly avoided in modern-day code. Again, if you want to write code to handle that situation (e.g., with a CER), then feel free; I do not believe it worthwhile.Steeve
T
8

The problems in your implementation arise from your desire to remove unused lockers from the dictionary. It would be much simpler if you could just let each SemaphoreSlim stay in the dictionary forever (until the process terminates). Assuming that this is not a viable option, you have two obstacles to overcome:

  1. How to keep track of how many workers are using each semaphore, so that you know when it's safe to remove it.
  2. How to do the above using the performant but tricky ConcurrentDictionary<K,V> collection.

Stephen Cleary's answer shows how to solve the first problem, using a normal Dictionary<K,V>. A reference counter is stored along with each SemaphoreSlim, and everything is synchronized with the lock statement on a single locker object. In this answer I'll show how to solve the second problem.

The problem with the ConcurrentDictionary<K,V> collection is that it protects from corruption only its internal state, not the values it contains. So if you use a mutable class as TValue, you are opening the door for subtle race conditions, especially if you intend to cache these values in a pool and reuse them. The trick that eliminates the race conditions is to make the TValue an immutable struct. This way it essentially becomes part of the internal state of the dictionary, and it is protected by it. In the AsyncDuplicateLock implementation below, the TValue is a readonly struct, declared also as a record for performance¹ and convenience:

public class AsyncDuplicateLock
{
    private readonly ConcurrentDictionary<object, Entry> _semaphores = new();

    private readonly record struct Entry(SemaphoreSlim Semaphore, int RefCount);

    public readonly struct Releaser : IDisposable
    {
        private readonly AsyncDuplicateLock _parent;
        private readonly object _key;
        public Releaser(AsyncDuplicateLock parent, object key)
        {
            _parent = parent; _key = key;
        }
        public void Dispose() => _parent.Release(_key);
    }

    public async ValueTask<Releaser> LockAsync(object key)
    {
        Entry entry = _semaphores.AddOrUpdate(key,
            static _ => new Entry(new SemaphoreSlim(1, 1), 1),
            static (_, entry) => entry with { RefCount = entry.RefCount + 1 });

        await entry.Semaphore.WaitAsync().ConfigureAwait(false);
        return new Releaser(this, key);
    }

    private void Release(object key)
    {
        Entry entry;
        while (true)
        {
            bool exists = _semaphores.TryGetValue(key, out entry);
            if (!exists)
                throw new InvalidOperationException("Key not found.");
            if (entry.RefCount > 1)
            {
                Entry newEntry = entry with { RefCount = entry.RefCount - 1 };
                if (_semaphores.TryUpdate(key, newEntry, entry))
                    break;
            }
            else
            {
                if (_semaphores.TryRemove(KeyValuePair.Create(key, entry)))
                    break;
            }
        }
        entry.Semaphore.Release();
    }
}

Notice that increasing and decreasing the RefCount involves spinning in a while loop. That's because the current thread might lose the optimistic race with other threads for updating the dictionary, in which case it tries again until it succeeds. The spinning is obvious in the Release method, but also happens internally in the LockAsync method. The AddOrUpdate method employs internally a similar logic around the invocation of the updateValueFactory delegate.

Performance: the above implementation is about 80% faster than a simpler Dictionary<K,V>-based implementation, under conditions of heavy contention. That's because the ConcurrentDictionary<K,V> utilizes multiple locker objects internally, so a thread that wants to lock on the key "A" doesn't have to wait until another thread completes acquiring or releasing the key "B". It is considerably more allocatey though. If you have some reason to keep the garbage collector relaxed, a Dictionary<K,V>-based implementation will you serve you better. If you desire both ultimate speed and ultimate memory-efficiency, you could take a look at the 6th revision of this answer, for an implementation based on multiple Dictionary<K,V>s.

Exceptions: When the SemaphoreSlim class is misused, it throws a SemaphoreFullException. This happens when the semaphore is released more times than it has been acquired. The AsyncDuplicateLock implementation of this answer behaves differently in case of misuse: it throws an InvalidOperationException("Key not found."). This happens because when a key is released as many times as it has been acquired, the associated semaphore is removed from the dictionary. If this implementation ever throws a SemaphoreFullException, it would be an indication of a bug.

Note: Personally I am not a fan of (mis)using the using statement for purposes other than releasing unmanaged resources.

¹ The ConcurrentDictionary<K,V> compares the TValues in many operations (AddOrUpdate, TryUpdate and TryRemove among others), using the EqualityComparer<TValue>.Default. Structs by default are not compared efficiently, unless they implement the IEquatable<T> interface. Record structs do implement this interface, in a similar way to the value-tuples, so they can be compared for equality efficiently. Actually using a value-tuple as TValue ((SemaphoreSlim, int)) might be slightly more efficient, because the members of value-tuples are fields, while the members of record structs are properties. Record structs are more convenient though.

Thelen answered 11/12, 2020 at 17:43 Comment(10)
For a non-async variant of the same idea, take a look at this answer.Thelen
An example of how to use this class would be helpfulJaleesa
@DeivydasVoroneckis there is an example in the question itself (under the caption "Usage - within a HttpModule"). The API of the AsyncDuplicateLock in this answer is almost identical with the API of the AsyncDuplicateLock inside the question.Thelen
Is the actual SemaphoreSlim ever .Dispose()d here? I can't see where that happens. Though that it should be done after it's removed from the dictionary.Isabellaisabelle
​@Isabellaisabelle no, the SemaphoreSlims are not disposed. In Stephen Cleary's answer they are not disposed either. Disposing SemaphoreSlims is not really important, unless the AvailableWaitHandle property is used, which is not in the above implementation. If you want to ensure that each and every SemaphoreSlim is disposed, it can be tricky because the AddOrUpdate method can invoke the addValueFactory delegate, and then silently discard the resulting value.Thelen
@TheodorZoulias I see, seems like you don't always need to dispose of the SemaphoreSlim. Would be great if that was documented in the docs. Regarding it being safe to call Dispose after it was removed and released: if AddOrUpdate discards the created value, it means that the value didn't end up being used. So shouldn't hurt if we .Dispose()d of the semaphore?Isabellaisabelle
​@Isabellaisabelle the Task.Dispose is documented to be basically superfluous, but the SemaphoreSlim.Dispose is not. You have to rely on the shared wisdom of the community about this. Regarding how to solve the AddOrUpdate problem, when you are inside the addValueFactory you don't know if the returned value will be discarded or not, so you can't call Dispose there. You may have to abandon the convenience of the AddOrUpdate, and do it at a lower lever.Thelen
@TheodorZoulias but if .TryRemove() returns true, doesn't that mean that that specific semaphore got removed, thus it wasn't and isn't used by anyone else, and thus can safely be .Dispose()d of?Isabellaisabelle
@Isabellaisabelle yes, you can safely dispose the semaphores that are removed from the dictionary. I didn't bother doing it to keep the code simple, and to not give the false impression that my code disposes ALL the instantiated semaphores.Thelen
@Isabellaisabelle the reason that it is safe to dispose a semaphore that is removed from the dictionary, is because only one thread at a time is allowed to enter the semaphore, and subsequently Release it. If we change the instantiation from new SemaphoreSlim(1,1) to new SemaphoreSlim(2,2), then two threads could Release the semaphore concurrently, so it won't be safe for the thread that removed it from the dictionary to Dispose it after the Release, because the other thread might Release it later.Thelen
L
3

I wrote a library called AsyncKeyedLock to fix this common problem. The library currently supports using it with the type object (so you can mix different types together) or using generics to get a more efficient solution. It allows for timeouts, cancellation tokens, and also pooling so as to reduce allocations. Underlying it uses a ConcurrentDictionary and also allows for setting the initial capacity and concurrency for this dictionary.

I have benchmarked this against the other solutions provided here and it is more efficient, in terms of speed, memory usage (allocations) as well as scalability (internally it uses the more scalable ConcurrentDictionary). It's being used in a number of systems in production and used by a number of popular libraries.

The source code is available on GitHub and packaged at NuGet.

The approach here is to basically use the ConcurrentDictionary to store an IDisposable object which has a counter on it and a SemaphoreSlim. Once this counter reaches 0, it is removed from the dictionary and either disposed or returned to the pool (if pooling is used). Monitor is used to lock this object when either the counter is being incremented or decremented.

Usage example:

var locker = new AsyncKeyedLocker<string>(o =>
{
   o.PoolSize = 20;
   o.PoolInitialFill = 1;
});

string key = "my key";

// asynchronous code
using (await locker.LockAsync(key, cancellationToken))
{
   ...
}

// synchronous code
using (locker.Lock(key))
{
   ...
}

Download from NuGet.

Laurent answered 20/11, 2022 at 9:34 Comment(0)
B
0

I rewrote the @StephenCleary answer with this:

public sealed class AsyncLockList {

    readonly Dictionary<object, SemaphoreReferenceCount> Semaphores = new Dictionary<object, SemaphoreReferenceCount>();

    SemaphoreSlim GetOrCreateSemaphore(object key) {
        lock (Semaphores) {
            if (Semaphores.TryGetValue(key, out var item)) {
                item.IncrementCount();
            } else {
                item = new SemaphoreReferenceCount();
                Semaphores[key] = item;
            }
            return item.Semaphore;
        }
    }

    public IDisposable Lock(object key) {
        GetOrCreateSemaphore(key).Wait();
        return new Releaser(Semaphores, key);
    }

    public async Task<IDisposable> LockAsync(object key) {
        await GetOrCreateSemaphore(key).WaitAsync().ConfigureAwait(false);
        return new Releaser(Semaphores, key);
    }

    sealed class SemaphoreReferenceCount {
        public readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
        public int Count { get; private set; } = 1;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void IncrementCount() => Count++;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void DecrementCount() => Count--;
    }

    sealed class Releaser : IDisposable {
        readonly Dictionary<object, SemaphoreReferenceCount> Semaphores;
        readonly object Key;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public Releaser(Dictionary<object, SemaphoreReferenceCount> semaphores, object key) {
            Semaphores = semaphores;
            Key = key;
        }

        public void Dispose() {
            lock (Semaphores) {
                var item = Semaphores[Key];
                item.DecrementCount();
                if (item.Count == 0)
                    Semaphores.Remove(Key);
                item.Semaphore.Release();
            }
        }
    }
}
Boastful answered 28/8, 2018 at 5:45 Comment(3)
@stephencleary, I'm more comfortable with the code rewritten this way, can you comment on any possible significant inefficiencies I may have introduced?Boastful
I copy-paste your answer in my solution. I liked more how you rewrited your code. However, it doesn't work. I can't tell you why but no lock happenes for the same key. Dark Falcon answer worked out of the box!Brotherton
I meant @Stephen ClearyBrotherton
C
-1

Inspired by this previous answer, here is a version that supports async wait:

    public class KeyedLock<TKey>
    {
        private readonly ConcurrentDictionary<TKey, LockInfo> _locks = new();

        public int Count => _locks.Count;

        public async Task<IDisposable> WaitAsync(TKey key, CancellationToken cancellationToken = default)
        {
            // Get the current info or create a new one.
            var info = _locks.AddOrUpdate(key,
                // Add
                k => new LockInfo(),
                // Update
                (k, v) => v.Enter() ? v : new LockInfo());

            try
            {
                await info.Semaphore.WaitAsync(cancellationToken);

                return new Releaser(() => Release(key, info, true));
            }
            catch (OperationCanceledException)
            {
                // The semaphore wait was cancelled, release the lock.
                Release(key, info, false);
                throw;
            }
        }

        private void Release(TKey key, LockInfo info, bool isCurrentlyLocked)
        {
            if (info.Leave())
            {
                // This was the last lock for the key.

                // Only remove this exact info, in case another thread has
                // already put its own info into the dictionary
                // Note that this call to Remove(entry) is in fact thread safe.
                var entry = new KeyValuePair<TKey, LockInfo>(key, info);
                if (((ICollection<KeyValuePair<TKey, LockInfo>>)_locks).Remove(entry))
                {
                    // This exact info was removed.
                    info.Dispose();
                }
            }
            else if (isCurrentlyLocked)
            {
                // There is another waiter.
                info.Semaphore.Release();
            }
        }

        private class LockInfo : IDisposable
        {
            private SemaphoreSlim _semaphore = null;
            private int _refCount = 1;

            public SemaphoreSlim Semaphore
            {
                get
                {
                    // Lazily create the semaphore.
                    var s = _semaphore;
                    if (s is null)
                    {
                        s = new SemaphoreSlim(1, 1);

                        // Assign _semaphore if its current value is null.
                        var original = Interlocked.CompareExchange(ref _semaphore, s, null);

                        // If someone else already created a semaphore, return that one
                        if (original is not null)
                        {
                            s.Dispose();
                            return original;
                        }
                    }
                    return s;
                }
            }

            // Returns true if successful
            public bool Enter()
            {
                if (Interlocked.Increment(ref _refCount) > 1)
                {
                    return true;
                }

                // This lock info is not valid anymore - its semaphore is or will be disposed.
                return false;
            }

            // Returns true if this lock info is now ready for removal
            public bool Leave()
            {
                if (Interlocked.Decrement(ref _refCount) <= 0)
                {
                    // This was the last lock
                    return true;
                }

                // There is another waiter
                return false;
            }

            public void Dispose() => _semaphore?.Dispose();
        }

        private sealed class Releaser : IDisposable
        {
            private readonly Action _dispose;

            public Releaser(Action dispose) => _dispose = dispose;

            public void Dispose() => _dispose();
        }
    }
Chilt answered 6/1, 2022 at 22:6 Comment(1)
The AddOrUpdate method might invoke the updateValueFactory delegate more than one times. My understanding is that this implementation expects it to be called only once, in which case it is a buggy implementation.Thelen

© 2022 - 2024 — McMap. All rights reserved.