Preview:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace AppsensPro.Infrastructure.Threading
{
    public sealed class KeyedLock<TKey>
    {
        /// <summary>
        /// Simple wrapper to allow counting how many references an object has
        /// </summary>
        /// <typeparam name="T"></typeparam>
        private sealed class RefCounted<T>
        {
            public RefCounted(T value)
            {
                RefCount = 1;
                Value = value;
            }

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

        /// <summary>
        /// Internally maintained dictionary of <see cref="SemaphoreSlim"/>
        /// </summary>
        private readonly Dictionary<TKey, RefCounted<SemaphoreSlim>> _semaphoreSlims
            = new Dictionary<TKey, RefCounted<SemaphoreSlim>>();

        /// <summary>
        /// Atomically get or create a value, with the associated <see cref="RefCounted{T}"/> incremented
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        private SemaphoreSlim GetOrCreate(TKey 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;
        }

        /// <summary>
        /// Lock with specified key, returning a disposable <see cref="Releaser"/>
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public IDisposable Lock(TKey key)
        {
            GetOrCreate(key).Wait();
            return new Releaser(key, _semaphoreSlims);
        }

        /// <summary>
        /// Asynchronously lock with specified key, returning a disposable <see cref="Releaser"/>
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public async Task<IDisposable> LockAsync(TKey key)
        {
            await GetOrCreate(key).WaitAsync().ConfigureAwait(false);
            return new Releaser(key, _semaphoreSlims);
        }

        /// <summary>
        /// The returned Disposable <see cref="Releaser"/>.
        /// The lock will only be removed from dictionary if exists no reference left
        /// </summary>
        private sealed class Releaser : IDisposable
        {
            private TKey _key;
            private readonly Dictionary<TKey, RefCounted<SemaphoreSlim>> _locks;

            public Releaser(TKey key, Dictionary<TKey, RefCounted<SemaphoreSlim>> locks)
            {
                _key = key;
                _locks = locks;
            }
            
            public void Dispose()
            {
                RefCounted<SemaphoreSlim> item;
                lock (_locks)
                {
                    item = _locks[_key];
                    --item.RefCount;
                    if (item.RefCount == 0)
                        _locks.Remove(_key);
                }
                item.Value.Release();
            }
        }
    }
}
downloadDownload PNG downloadDownload JPEG downloadDownload SVG

Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!

Click to optimize width for Twitter