From 068a568bcc368b4f19fc62a9d5e5b19b418e7ca1 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 13:07:30 +0800 Subject: [PATCH] Enhance KeyedLock with additional locking methods and improve documentation --- .../Volo/Abp/Threading/KeyedLock.cs | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs index b7871cb0e7..7cab60c8d7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs @@ -6,20 +6,58 @@ using System.Threading.Tasks; namespace Volo.Abp.Threading; /// -/// Per-key asynchronous lock. -/// https://stackoverflow.com/a/31194647 +/// Per-key asynchronous lock for coordinating concurrent flows. /// +/// +/// Based on the pattern described in https://stackoverflow.com/a/31194647. +/// Use within a using scope to ensure the lock is released via IDisposable.Dispose(). +/// public static class KeyedLock { private static readonly Dictionary> SemaphoreSlims = new(); + /// + /// Acquires an exclusive asynchronous lock for the specified . + /// This method waits until the lock becomes available. + /// + /// A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock. + /// An handle that must be disposed to release the lock. + /// Thrown when is . + /// + /// + /// var key = "my-critical-section"; + /// using (await KeyedLock.LockAsync(key)) + /// { + /// // protected work + /// } + /// + /// public static async Task LockAsync(object key) { + Check.NotNull(key, nameof(key)); return await LockAsync(key, CancellationToken.None); } + /// + /// Acquires an exclusive asynchronous lock for the specified , observing a . + /// + /// A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock. + /// A token to cancel the wait for the lock. + /// An handle that must be disposed to release the lock. + /// Thrown when is . + /// Thrown if the wait is canceled via . + /// + /// + /// var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + /// using (await KeyedLock.LockAsync("db-update", cts.Token)) + /// { + /// // protected work + /// } + /// + /// public static async Task LockAsync(object key, CancellationToken cancellationToken) { + Check.NotNull(key, nameof(key)); var semaphore = GetOrCreate(key); try { @@ -34,13 +72,62 @@ public static class KeyedLock return new Releaser(key); } + /// + /// Attempts to acquire an exclusive lock for the specified without waiting. + /// + /// A non-null object that identifies the lock. + /// + /// An handle if the lock was immediately acquired; otherwise . + /// + /// Thrown when is . + /// + /// + /// var handle = await KeyedLock.TryLockAsync("cache-key"); + /// if (handle != null) + /// { + /// using (handle) + /// { + /// // protected work + /// } + /// } + /// + /// public static async Task TryLockAsync(object key) { + Check.NotNull(key, nameof(key)); return await TryLockAsync(key, default, CancellationToken.None); } + /// + /// Attempts to acquire an exclusive lock for the specified , waiting up to . + /// + /// A non-null object that identifies the lock. + /// Maximum time to wait for the lock. If set to , the method performs an immediate, non-blocking attempt. + /// A token to cancel the wait. + /// + /// An handle if the lock was acquired within the timeout; otherwise . + /// + /// Thrown when is . + /// Thrown if the wait is canceled via . + /// + /// + /// var handle = await KeyedLock.TryLockAsync("send-mail", TimeSpan.FromSeconds(1)); + /// if (handle != null) + /// { + /// using (handle) + /// { + /// // protected work + /// } + /// } + /// else + /// { + /// // lock not acquired within timeout + /// } + /// + /// public static async Task TryLockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken = default) { + Check.NotNull(key, nameof(key)); var semaphore = GetOrCreate(key); bool acquired; try