Created
July 1, 2015 07:20
-
-
Save JimBobSquarePants/208ff72e0a93abca4043 to your computer and use it in GitHub Desktop.
Revisions
-
JimBobSquarePants created this gist
Jul 1, 2015 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,117 @@ /// <summary> /// Throttles duplicate requests. /// Based loosely on <see href="http://stackoverflow.com/a/21011273/427899"/> /// </summary> 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); } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,13 @@ The issue is as follows. For a given key, - Thread 1 calls GetOrAdd and adds a new semaphore and acquires it via Wait - Thread 2 calls GetOrAdd and gets the existing semaphore and blocks on Wait - Thread 1 releases the semaphore, only after having called TryRemove, which removed the semaphore from the dictionary - Thread 2 now acquires the semaphore. - Thread 3 calls GetOrAdd for the same key as thread 1 and 2. Thread 2 is still holding the semaphore, but the semaphore is not in the dictionary, so thread 3 creates a new semaphore and both threads 2 and 3 access the same protected resource. Using a Semaphoreslim to lock has precedents. See http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx I was attempting to block only when a duplicate occurs but obviously this doesn't work.