Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asynchronous locking based on a key

I'm attempting to figure out an issue that has been raised with my ImageProcessor library here where I am getting intermittent file access errors when adding items to the cache.

System.IO.IOException: The process cannot access the file 'D:\home\site\wwwroot\app_data\cache\0\6\5\f\2\7\065f27fc2c8e843443d210a1e84d1ea28bbab6c4.webp' because it is being used by another process.

I wrote a class designed to perform an asynchronous lock based upon a key generated by a hashed url but it seems I have missed something in the implementation.

My locking class

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

Usage - within a HttpModule

private readonly AsyncDuplicateLock locker = new AsyncDuplicateLock();  using (await this.locker.LockAsync(cachedPath)) {     // Process and save a cached image. } 

Can anyone spot where I have gone wrong? I'm worried that I am misunderstanding something fundamental.

The full source for the library is stored on Github here

like image 809
James South Avatar asked Jun 30 '15 12:06

James South


People also ask

Can we use lock in async await?

The await keyword in C# (. NET Async CTP) is not allowed from within a lock statement. From MSDN: An await expression cannot be used in a synchronous function, in a query expression, in the catch or finally block of an exception handling statement, in the block of a lock statement, or in an unsafe context.

What is SemaphoreSlim C#?

Semaphore class in System. Threading is a thin wrapper around the Win32 Semaphore object. This is used to controls access to a resource or pool of resources concurrently. SemaphoreSlim class is lightweight and faster than Semaphore, as it limited to a single process.


2 Answers

As the other answerer noted, the original code is removing the SemaphoreSlim from the ConcurrentDictionary before it releases the semaphore. So, you've got too much semaphore churn going on - they're being removed from the dictionary when they could still be in use (not acquired, but already retrieved from the dictionary).

The problem with this kind of "mapping lock" is that it's difficult to know when the semaphore is no longer necessary. One option is to never dispose the semaphores at all; that's the easy solution, but may not be acceptable in your scenario. Another option - if the semaphores are actually related to object instances and not values (like strings) - is to attach them using ephemerons; however, I believe this option would also not be acceptable in your scenario.

So, we do it the hard way. :)

There are a few different approaches that would work. I think it makes sense to approach it from a reference-counting perspective (reference-counting each semaphore in the dictionary). Also, we want to make the decrement-count-and-remove operation atomic, so I just use a single lock (making the concurrent dictionary superfluous):

public sealed class AsyncDuplicateLock {   private sealed class RefCounted<T>   {     public RefCounted(T value)     {       RefCount = 1;       Value = value;     }      public int RefCount { get; set; }     public T Value { get; private set; }   }    private static readonly Dictionary<object, RefCounted<SemaphoreSlim>> SemaphoreSlims                         = new Dictionary<object, RefCounted<SemaphoreSlim>>();    private SemaphoreSlim GetOrCreate(object key)   {     RefCounted<SemaphoreSlim> item;     lock (SemaphoreSlims)     {       if (SemaphoreSlims.TryGetValue(key, out item))       {         ++item.RefCount;       }       else       {         item = new RefCounted<SemaphoreSlim>(new SemaphoreSlim(1, 1));         SemaphoreSlims[key] = item;       }     }     return item.Value;   }    public IDisposable Lock(object key)   {     GetOrCreate(key).Wait();     return new Releaser { Key = key };   }    public async Task<IDisposable> LockAsync(object key)   {     await GetOrCreate(key).WaitAsync().ConfigureAwait(false);     return new Releaser { Key = key };   }    private sealed class Releaser : IDisposable   {     public object Key { get; set; }      public void Dispose()     {       RefCounted<SemaphoreSlim> item;       lock (SemaphoreSlims)       {         item = SemaphoreSlims[Key];         --item.RefCount;         if (item.RefCount == 0)           SemaphoreSlims.Remove(Key);       }       item.Value.Release();     }   } } 
like image 172
Stephen Cleary Avatar answered Sep 20 '22 17:09

Stephen Cleary


For a given key,

  1. Thread 1 calls GetOrAdd and adds a new semaphore and acquires it via Wait
  2. Thread 2 calls GetOrAdd and gets the existing semaphore and blocks on Wait
  3. Thread 1 releases the semaphore, only after having called TryRemove, which removed the semaphore from the dictionary
  4. Thread 2 now acquires the semaphore.
  5. 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.

You need to adjust your logic. The semaphore should only be removed from the dictionary when it has no waiters.

Here is one potential solution, minus the async part:

public sealed class AsyncDuplicateLock {     private class LockInfo     {         private SemaphoreSlim sem;         private int waiterCount;          public LockInfo()         {             sem = null;             waiterCount = 1;         }          // Lazily create the semaphore         private SemaphoreSlim Semaphore         {             get             {                 var s = sem;                 if (s == null)                 {                     s = new SemaphoreSlim(0, 1);                     var original = Interlocked.CompareExchange(ref sem, null, s);                     // If someone else already created a semaphore, return that one                     if (original != null)                         return original;                 }                 return s;             }         }          // Returns true if successful         public bool Enter()         {             if (Interlocked.Increment(ref waiterCount) > 1)             {                 Semaphore.Wait();                 return true;             }             return false;         }          // Returns true if this lock info is now ready for removal         public bool Exit()         {             if (Interlocked.Decrement(ref waiterCount) <= 0)                 return true;              // There was another waiter             Semaphore.Release();             return false;         }     }      private static readonly ConcurrentDictionary<object, LockInfo> activeLocks = new ConcurrentDictionary<object, LockInfo>();      public static IDisposable Lock(object key)     {         // Get the current info or create a new one         var info = activeLocks.AddOrUpdate(key,           (k) => new LockInfo(),           (k, v) => v.Enter() ? v : new LockInfo());          DisposableScope releaser = new DisposableScope(() =>         {             if (info.Exit())             {                 // Only remove this exact info, in case another thread has                 // already put its own info into the dictionary                 ((ICollection<KeyValuePair<object, LockInfo>>)activeLocks)                   .Remove(new KeyValuePair<object, LockInfo>(key, info));             }         });          return releaser;     }      private sealed class DisposableScope : IDisposable     {         private readonly Action closeScopeAction;          public DisposableScope(Action closeScopeAction)         {             this.closeScopeAction = closeScopeAction;         }          public void Dispose()         {             this.closeScopeAction();         }     } } 
like image 24
Dark Falcon Avatar answered Sep 21 '22 17:09

Dark Falcon