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