基于密钥的异步锁定

时间:2015-06-30 12:20:31

标签: c# multithreading locking async-await imageprocessor

我试图弄清楚我的ImageProcessor库here引发的问题,我在向缓存添加项目时遇到间歇性的文件访问错误。

  

System.IO.IOException:进程无法访问该文件' D:\ home \ site \ wwwroot \ app_data \ cache \ 0 \ 6 \ 5 \ f \ 2 \ 7 \ 065f27fc2c8e843443d210a1e84d1ea28bbab6c4.webp'因为它正被另一个进程使用。

我编写了一个类,用于根据散列网址生成的密钥执行异步锁定,但似乎我在实现中遗漏了一些内容。

我的锁定类

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

用法 - 在HttpModule中

private readonly AsyncDuplicateLock locker = new AsyncDuplicateLock();

using (await this.locker.LockAsync(cachedPath))
{
    // Process and save a cached image.
}

有人可以找出我出错的地方吗?我担心我误解了一些基本的东西。

该库的完整源代码存储在Github here

4 个答案:

答案 0 :(得分:30)

作为other answerer noted,原始代码会在释放信号量之前从SemaphoreSlim中删除ConcurrentDictionary。所以,你有太多的信号量流失 - 当它们仍然可以使用时它们被从字典中移除(没有被获取,但已经从字典中检索出来)。

这种“映射锁定”的问题在于,很难知道何时不再需要信号量。一种选择是永远不要丢弃信号量;这是简单的解决方案,但在您的方案中可能无法接受。另一个选择 - 如果信号量实际上与对象实例相关而不是值(如字符串) - 是使用ephemerons附加它们;但是,我相信这个选项在你的场景中也是不可接受的。

所以,我们这样做很难。 :)

有一些不同的方法可行。我认为从引用计数的角度来看它是有意义的(引用计算字典中的每个信号量)。此外,我们希望将减量计数和删除操作设为原子,因此我只使用单个lock(使并发字典变得多余):

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

答案 1 :(得分:1)

对于给定的密钥,

  1. 主题1调用GetOrAdd并添加新信号量并通过Wait
  2. 获取
  3. 线程2调用GetOrAdd并获取现有信号量并阻止Wait
  4. 线程1仅在调用TryRemove后才释放信号量,该信号从字典中删除了信号量
  5. 线程2现在获取信号量。
  6. 线程3调用GetOrAdd获取与线程1和2相同的键。线程2仍然保持信号量,但信号量不在字典中,因此线程3创建一个新的信号量,并且线程2和3访问相同的受保护资源。
  7. 您需要调整逻辑。只有在没有服务员的情况下,才能从字典中删除信号量。

    这是一个可能的解决方案,减去异步部分:

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

答案 2 :(得分:0)

我用以下代码重写了@StephenCleary答案:

public sealed class AsyncLockList {

    readonly Dictionary<object, SemaphoreReferenceCount> Semaphores = new Dictionary<object, SemaphoreReferenceCount>();

    SemaphoreSlim GetOrCreateSemaphore(object key) {
        lock (Semaphores) {
            if (Semaphores.TryGetValue(key, out var item)) {
                item.IncrementCount();
            } else {
                item = new SemaphoreReferenceCount();
                Semaphores[key] = item;
            }
            return item.Semaphore;
        }
    }

    public IDisposable Lock(object key) {
        GetOrCreateSemaphore(key).Wait();
        return new Releaser(Semaphores, key);
    }

    public async Task<IDisposable> LockAsync(object key) {
        await GetOrCreateSemaphore(key).WaitAsync().ConfigureAwait(false);
        return new Releaser(Semaphores, key);
    }

    sealed class SemaphoreReferenceCount {
        public readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
        public int Count { get; private set; } = 1;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void IncrementCount() => Count++;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void DecrementCount() => Count--;
    }

    sealed class Releaser : IDisposable {
        readonly Dictionary<object, SemaphoreReferenceCount> Semaphores;
        readonly object Key;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public Releaser(Dictionary<object, SemaphoreReferenceCount> semaphores, object key) {
            Semaphores = semaphores;
            Key = key;
        }

        public void Dispose() {
            lock (Semaphores) {
                var item = Semaphores[Key];
                item.DecrementCount();
                if (item.Count == 0)
                    Semaphores.Remove(Key);
                item.Semaphore.Release();
            }
        }
    }
}

答案 3 :(得分:0)

这里是一个KeyedLock类,与Stephen Cleary的AsyncDuplicateLock相比,它更不方便且更容易出错,但分配性也较低。它在内部维护SemaphoreSlim的池,在由前一个密钥释放它们之后,任何密钥都可以重用它们。池的容量是可配置的,默认情况下为10。

该类不是无分配的,因为SemaphoreSlim类每次由于争用而无法同步获取信号量时都会分配内存(实际上很多)。

可以同步和异步请求锁定,也可以通过取消和超时请求锁定。这些功能是通过利用SemaphoreSlim类的现有功能来提供的。

public class KeyedLock<TKey>
{
    private readonly Dictionary<TKey, (SemaphoreSlim, int)> _perKey;
    private readonly Stack<SemaphoreSlim> _pool;
    private readonly int _poolCapacity;

    public KeyedLock(IEqualityComparer<TKey> keyComparer = null, int poolCapacity = 10)
    {
        _perKey = new Dictionary<TKey, (SemaphoreSlim, int)>(keyComparer);
        _pool = new Stack<SemaphoreSlim>(poolCapacity);
        _poolCapacity = poolCapacity;
    }

    public Task WaitAsync(TKey key, CancellationToken cancellationToken = default)
        => GetSemaphore(key).WaitAsync(cancellationToken);

    public async Task<bool> WaitAsync(TKey key, int millisecondsTimeout,
        CancellationToken cancellationToken = default)
    {
        var semaphore = GetSemaphore(key);
        bool entered = await semaphore.WaitAsync(millisecondsTimeout,
            cancellationToken).ConfigureAwait(false);
        if (!entered) ReleaseSemaphore(key, entered: false);
        return entered;
    }

    public void Wait(TKey key, CancellationToken cancellationToken = default)
        => GetSemaphore(key).Wait(cancellationToken);

    public bool Wait(TKey key, int millisecondsTimeout,
        CancellationToken cancellationToken = default)
    {
        var semaphore = GetSemaphore(key);
        bool entered = semaphore.Wait(millisecondsTimeout, cancellationToken);
        if (!entered) ReleaseSemaphore(key, entered: false);
        return entered;
    }

    public void Release(TKey key) => ReleaseSemaphore(key, entered: true);

    private SemaphoreSlim GetSemaphore(TKey key)
    {
        SemaphoreSlim semaphore; int counter;
        lock (_perKey)
        {
            if (_perKey.TryGetValue(key, out var entry))
            {
                (semaphore, counter) = entry;
                _perKey[key] = (semaphore, ++counter);
            }
            else
            {
                bool rented; lock (_pool) rented = _pool.TryPop(out semaphore);
                if (!rented) semaphore = new SemaphoreSlim(1, 1);
                _perKey[key] = (semaphore, 1);
            }
        }
        return semaphore;
    }

    private void ReleaseSemaphore(TKey key, bool entered)
    {
        SemaphoreSlim semaphore; int counter;
        lock (_perKey)
        {
            if (_perKey.TryGetValue(key, out var entry))
            {
                (semaphore, counter) = entry;
                counter--;
                if (counter == 0)
                    _perKey.Remove(key);
                else
                    _perKey[key] = (semaphore, counter);
            }
            else
            {
                throw new InvalidOperationException("Key not found.");
            }
        }
        if (entered) semaphore.Release();
        if (counter == 0)
            lock (_pool) if (_pool.Count < _poolCapacity) _pool.Push(semaphore);
    }
}

用法示例:

var locker = new KeyedLock<string>();

await locker.WaitAsync("Hello");
try
{
    await DoSomethingAsync();
}
finally
{
    locker.Release("Hello");
}

该实现使用tuple deconstruction,至少需要C#7。

可以很容易地将KeyedLock类修改为KeyedSemaphore,这将允许每个键进行多个并发操作。在构造函数中只需要一个maximumConcurrencyPerKey参数,该参数将被存储并传递给SemaphoreSlim s的构造函数。