AsyncEx icon indicating copy to clipboard operation
AsyncEx copied to clipboard

Named lock list

Open bboyle1234 opened this issue 7 years ago • 5 comments

Would you consider adding this to the library? Again, adapted from stephen cleary code found on stackoverflow and used regularly in my own libraries :)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Apex.TaskUtilities {

    /// <summary>
    /// Provides access to named asynchronous and synchronous locks. 
    /// IMPORTANT!! These locks are NOT re-entrant.
    /// IMPORTANT!! Does not work cross-app-domain because SemaphoreSLIM is used internally.
    /// </summary>
    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;
            }
        }

        /// <summary>
        /// Synchronously blocks execution until the lock for the given key becomes available, and then blocks other
        /// requests until the IDisposable has been disposed. Useage:
        /// <code>
        /// using (lockList.Lock("nameOfTheLock")) { 
        ///   // do stuff inside lock
        /// }
        /// </code>
        /// IMPORTANT!! These locks are NOT re-entrant.
        /// IMPORTANT!! Does not work cross-app-domain because SemaphoreSLIM is used internally.
        /// </summary>
        /// <param name="key">A key to identify the requested lock.</param>
        /// <returns>An object which must be disposed to release the lock.</returns>
        public IDisposable Lock(object key) {
            GetOrCreateSemaphore(key).Wait();
            return new Releaser(Semaphores, key);
        }

        /// <summary>
        /// Asynchronously blocks execution until the lock for the given key becomes available, and then blocks other
        /// requests until the IDisposable has been disposed. Useage:
        /// <code>
        /// using (await lockList.LockAsync("nameOfTheLock")) { 
        ///   // do stuff inside lock
        /// }
        /// </code>
        /// IMPORTANT!! These locks are NOT re-entrant.
        /// IMPORTANT!! Does not work cross-app-domain because SemaphoreSLIM is used internally.
        /// </summary>
        /// <param name="key">A key to identify the requested lock.</param>
        /// <returns>An object which must be disposed to release the lock.</returns>
        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();
                }
            }
        }
    }
}

bboyle1234 avatar Oct 12 '18 02:10 bboyle1234

I'll consider it, but any changes like this need to be made across the whole library - i.e., we'd also need a named AsyncSemaphore, named AsyncManualResetEvent, etc.

StephenCleary avatar Oct 12 '18 12:10 StephenCleary

See also https://github.com/StephenCleary/AsyncEx/pull/16

I'm wondering if it would be possible to create some kind of naming wrapper?

StephenCleary avatar Dec 29 '18 21:12 StephenCleary

I like this comment of yours from #16

Usually, when developers want a semaphore (or AsyncSemaphore) for each object, they just include it in the object itself

If I look over the code I wrote that uses the above object, I would probably find that I should have included an AsyncLock object in the context of the object being dealt with instead of creating a centralized repository of keyed locks.

bboyle1234 avatar Jan 28 '19 00:01 bboyle1234

Another reference-counted approach: https://stackoverflow.com/a/31194647/263693

StephenCleary avatar Mar 23 '19 01:03 StephenCleary

  1. Both of these approaches lock the whole ConcurrentDictionary to perform an atomic decrement-count-and-remove operation. I was wondering if this is really necessary? or could you just lock the RefCounted item instead, and use Interlocked.Decrement() and Interlocked.Increment() to safely change the reference count and remove the item? What am I missing?

  2. Also, why do you would use a SemaphoreSlim instead of a Monitor? Is this just for Async support?

CameronWills avatar Oct 23 '19 04:10 CameronWills