|
/// <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); |
|
} |
|
} |
|
} |
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?