Skip to content

Instantly share code, notes, and snippets.

@JimBobSquarePants
Created July 1, 2015 07:20
Show Gist options
  • Save JimBobSquarePants/208ff72e0a93abca4043 to your computer and use it in GitHub Desktop.
Save JimBobSquarePants/208ff72e0a93abca4043 to your computer and use it in GitHub Desktop.
AsyncDuplicateLock
/// <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);
}
}
}

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.

@zpqrtbnk
Copy link

zpqrtbnk commented Jul 1, 2015

One thing to consider is whether you want to protect threads within an app domain, or accross processes and app domains (eg when an app domain is recycled and another one is starting...). SemaphoreSlim, the way you use it, is OK within the app domain, not accross app domains.

Next... IIC you want a "lockable thing" per key, correct? So only 1 thread at a time can lock on one key?

@zpqrtbnk
Copy link

zpqrtbnk commented Jul 1, 2015

Oh and I guess you want it async, so the lock method should in reality be an async method, that you can await, right?

@JimBobSquarePants
Copy link
Author

Can two app domains running a single website exist at the same time pointing to a single location? I guess with virtual paths that is possible but it is not something I've considered. I'm ok with locally scoped at the moment though as I'm more concerned about getting the lock itself correct. Definitely need to look at cross app domain though.

So yeah.. I want to lock per key. And most definitely Async.

Basically the process should be.

  • Thread 1 starts processing an image matching a given key - not blocked.
  • Thread 2 starts processing an image matching a second key - not blocked.
  • Thread 3 starts to process an image matching the first key. This now has to wait for Thread 1 to finish.
  • Thread 4 starts processing an image matching a fourth key - not blocked.

@dittodhole
Copy link

dittodhole commented May 20, 2016

Can two app domains running a single website exist at the same time pointing to a single location?

Sure they can - you just need to adapt the count of workers in IIS, which will spawn multiple processes for scaling 🍻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment