Skip to content

Instantly share code, notes, and snippets.

@JimBobSquarePants
Created July 1, 2015 07:20
Show Gist options
  • Select an option

  • Save JimBobSquarePants/208ff72e0a93abca4043 to your computer and use it in GitHub Desktop.

Select an option

Save JimBobSquarePants/208ff72e0a93abca4043 to your computer and use it in GitHub Desktop.

Revisions

  1. JimBobSquarePants created this gist Jul 1, 2015.
    117 changes: 117 additions & 0 deletions AsyncDuplicateLock.cs
    Original 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);
    }
    }
    }
    13 changes: 13 additions & 0 deletions Issue.md
    Original 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.