diff --git a/GFramework.Core.Abstractions/Concurrency/IAsyncKeyLockManager.cs b/GFramework.Core.Abstractions/Concurrency/IAsyncKeyLockManager.cs
new file mode 100644
index 0000000..4731057
--- /dev/null
+++ b/GFramework.Core.Abstractions/Concurrency/IAsyncKeyLockManager.cs
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using GFramework.Core.Abstractions.Utility;
+
+namespace GFramework.Core.Abstractions.Concurrency;
+
+///
+/// 异步键锁管理器接口,提供基于键的细粒度锁机制
+///
+public interface IAsyncKeyLockManager : IUtility, IDisposable
+{
+ ///
+ /// 异步获取指定键的锁(推荐使用)
+ ///
+ /// 锁键
+ /// 取消令牌
+ /// 锁句柄,使用 await using 自动释放
+ ValueTask AcquireLockAsync(string key, CancellationToken cancellationToken = default);
+
+ ///
+ /// 同步获取指定键的锁(兼容性方法)
+ ///
+ /// 锁键
+ /// 锁句柄,使用 using 自动释放
+ IAsyncLockHandle AcquireLock(string key);
+
+ ///
+ /// 获取锁管理器的统计信息
+ ///
+ /// 统计信息快照
+ LockStatistics GetStatistics();
+
+ ///
+ /// 获取当前活跃的锁信息(用于调试)
+ ///
+ /// 键到锁信息的只读字典
+ IReadOnlyDictionary GetActiveLocks();
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Concurrency/IAsyncLockHandle.cs b/GFramework.Core.Abstractions/Concurrency/IAsyncLockHandle.cs
new file mode 100644
index 0000000..1b7aca0
--- /dev/null
+++ b/GFramework.Core.Abstractions/Concurrency/IAsyncLockHandle.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2025 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace GFramework.Core.Abstractions.Concurrency;
+
+///
+/// 异步锁句柄接口,支持 await using 语法
+///
+public interface IAsyncLockHandle : IAsyncDisposable, IDisposable
+{
+ ///
+ /// 锁的键
+ ///
+ string Key { get; }
+
+ ///
+ /// 锁获取时的时间戳(Environment.TickCount64)
+ ///
+ long AcquiredTicks { get; }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Concurrency/LockStatistics.cs b/GFramework.Core.Abstractions/Concurrency/LockStatistics.cs
new file mode 100644
index 0000000..c875496
--- /dev/null
+++ b/GFramework.Core.Abstractions/Concurrency/LockStatistics.cs
@@ -0,0 +1,69 @@
+// Copyright (c) 2025 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace GFramework.Core.Abstractions.Concurrency;
+
+///
+/// 锁统计信息
+///
+public readonly struct LockStatistics
+{
+ ///
+ /// 当前活跃的锁数量
+ ///
+ public int ActiveLockCount { get; init; }
+
+ ///
+ /// 累计获取锁的次数
+ ///
+ public int TotalAcquired { get; init; }
+
+ ///
+ /// 累计释放锁的次数
+ ///
+ public int TotalReleased { get; init; }
+
+ ///
+ /// 累计清理的锁数量
+ ///
+ public int TotalCleaned { get; init; }
+}
+
+///
+/// 锁信息(用于调试)
+///
+public readonly struct LockInfo
+{
+ ///
+ /// 锁的键
+ ///
+ public string Key { get; init; }
+
+ ///
+ /// 当前引用计数
+ ///
+ public int ReferenceCount { get; init; }
+
+ ///
+ /// 最后访问时间戳(Environment.TickCount64)
+ ///
+ public long LastAccessTicks { get; init; }
+
+ ///
+ /// 等待队列长度(近似值)
+ /// 注意:这是一个基于 SemaphoreSlim.CurrentCount 的近似指示器,
+ /// 当 CurrentCount == 0 时表示锁被持有且可能有等待者,返回 1;
+ /// 否则返回 0。这不是精确的等待者数量,仅用于调试参考。
+ ///
+ public int WaitingCount { get; init; }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs b/GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs
new file mode 100644
index 0000000..e249013
--- /dev/null
+++ b/GFramework.Core.Tests/Concurrency/AsyncKeyLockManagerTests.cs
@@ -0,0 +1,337 @@
+// Copyright (c) 2025 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using GFramework.Core.Concurrency;
+
+namespace GFramework.Core.Tests.Concurrency;
+
+[TestFixture]
+public sealed class AsyncKeyLockManagerTests
+{
+ [Test]
+ public async Task AcquireLockAsync_Should_ReturnValidHandle()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+
+ // Act
+ await using var handle = await manager.AcquireLockAsync("test-key");
+
+ // Assert
+ Assert.That(handle, Is.Not.Null);
+ Assert.That(handle.Key, Is.EqualTo("test-key"));
+ Assert.That(handle.AcquiredTicks, Is.GreaterThan(0));
+ }
+
+ [Test]
+ public async Task AcquireLockAsync_WithSameKey_Should_SerializeAccess()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ var executionOrder = new List();
+ var tasks = new List();
+
+ // Act
+ for (var i = 0; i < 5; i++)
+ {
+ var index = i;
+ tasks.Add(Task.Run(async () =>
+ {
+ await using var handle = await manager.AcquireLockAsync("same-key");
+ executionOrder.Add(index);
+ await Task.Delay(10);
+ }));
+ }
+
+ await Task.WhenAll(tasks);
+
+ // Assert
+ Assert.That(executionOrder.Count, Is.EqualTo(5));
+ Assert.That(executionOrder.Distinct().Count(), Is.EqualTo(5));
+ }
+
+ [Test]
+ public async Task AcquireLockAsync_WithDifferentKeys_Should_AllowConcurrentAccess()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ var concurrentCount = 0;
+ var maxConcurrent = 0;
+ var tasks = new List();
+
+ // Act
+ for (var i = 0; i < 10; i++)
+ {
+ var key = $"key-{i}";
+ tasks.Add(Task.Run(async () =>
+ {
+ await using var handle = await manager.AcquireLockAsync(key);
+ var current = Interlocked.Increment(ref concurrentCount);
+ maxConcurrent = Math.Max(maxConcurrent, current);
+ await Task.Delay(50);
+ Interlocked.Decrement(ref concurrentCount);
+ }));
+ }
+
+ await Task.WhenAll(tasks);
+
+ // Assert
+ Assert.That(maxConcurrent, Is.GreaterThan(1));
+ }
+
+ [Test]
+ public async Task Dispose_Should_ReleaseHandle()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ var handle = await manager.AcquireLockAsync("test-key");
+
+ // Act
+ await handle.DisposeAsync();
+
+ // Assert - 应该能再次获取锁
+ await using var handle2 = await manager.AcquireLockAsync("test-key");
+ Assert.That(handle2, Is.Not.Null);
+ }
+
+ [Test]
+ public async Task ConcurrentAcquire_Should_NotThrowException()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ var tasks = new List();
+
+ // Act
+ for (var i = 0; i < 100; i++)
+ {
+ var key = $"key-{i % 10}";
+ tasks.Add(Task.Run(async () =>
+ {
+ await using var handle = await manager.AcquireLockAsync(key);
+ await Task.Delay(1);
+ }));
+ }
+
+ // Assert
+ Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks));
+ }
+
+ [Test]
+ public async Task ConcurrentAcquireSameKey_Should_SerializeAccess()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ var counter = 0;
+ var tasks = new List();
+
+ // Act
+ for (var i = 0; i < 100; i++)
+ {
+ tasks.Add(Task.Run(async () =>
+ {
+ await using var handle = await manager.AcquireLockAsync("same-key");
+ var temp = counter;
+ await Task.Delay(1);
+ counter = temp + 1;
+ }));
+ }
+
+ await Task.WhenAll(tasks);
+
+ // Assert
+ Assert.That(counter, Is.EqualTo(100));
+ }
+
+ [Test]
+ public async Task Cleanup_Should_RemoveUnusedLocks()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager(
+ cleanupInterval: TimeSpan.FromMilliseconds(100),
+ lockTimeout: TimeSpan.FromMilliseconds(200));
+
+ // Act
+ await using (var handle = await manager.AcquireLockAsync("temp-key"))
+ {
+ // 持有锁
+ }
+
+ // 等待清理
+ await Task.Delay(400);
+
+ var stats = manager.GetStatistics();
+
+ // Assert
+ Assert.That(stats.TotalCleaned, Is.GreaterThan(0));
+ }
+
+ [Test]
+ public async Task Cleanup_Should_NotRemoveActiveLocks()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager(
+ cleanupInterval: TimeSpan.FromMilliseconds(100),
+ lockTimeout: TimeSpan.FromMilliseconds(200));
+
+ // Act
+ await using var handle = await manager.AcquireLockAsync("active-key");
+
+ // 等待清理尝试
+ await Task.Delay(400);
+
+ var activeLocks = manager.GetActiveLocks();
+
+ // Assert
+ Assert.That(activeLocks.ContainsKey("active-key"), Is.True);
+ }
+
+ [Test]
+ public async Task GetStatistics_Should_ReturnCorrectCounts()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+
+ // Act
+ await using (await manager.AcquireLockAsync("key1"))
+ {
+ await using var handle2 = await manager.AcquireLockAsync("key2");
+ var stats = manager.GetStatistics();
+
+ // Assert
+ Assert.That(stats.TotalAcquired, Is.EqualTo(2));
+ Assert.That(stats.ActiveLockCount, Is.EqualTo(2));
+ }
+
+ var finalStats = manager.GetStatistics();
+ Assert.That(finalStats.TotalReleased, Is.EqualTo(2));
+ }
+
+ [Test]
+ public async Task GetActiveLocks_Should_ReturnCurrentLocks()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+
+ // Act
+ await using var handle1 = await manager.AcquireLockAsync("key1");
+ await using var handle2 = await manager.AcquireLockAsync("key2");
+
+ var activeLocks = manager.GetActiveLocks();
+
+ // Assert
+ Assert.That(activeLocks.Count, Is.EqualTo(2));
+ Assert.That(activeLocks.ContainsKey("key1"), Is.True);
+ Assert.That(activeLocks.ContainsKey("key2"), Is.True);
+ Assert.That(activeLocks["key1"].ReferenceCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void AcquireLockAsync_AfterDispose_Should_ThrowObjectDisposedException()
+ {
+ // Arrange
+ var manager = new AsyncKeyLockManager();
+ manager.Dispose();
+
+ // Act & Assert
+ Assert.ThrowsAsync(async () => await manager.AcquireLockAsync("test-key"));
+ }
+
+ [Test]
+ public async Task AcquireLockAsync_WithCancellation_Should_ThrowOperationCanceledException()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ using var cts = new CancellationTokenSource();
+
+ // 先获取锁
+ await using var handle = await manager.AcquireLockAsync("test-key", cts.Token);
+
+ // Act
+ await cts.CancelAsync();
+
+ // Assert
+ Assert.CatchAsync(async () =>
+ await manager.AcquireLockAsync("test-key", cts.Token));
+ }
+
+ [Test]
+ public void AcquireLock_Sync_Should_Work()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+
+ // Act
+ using var handle = manager.AcquireLock("test-key");
+
+ // Assert
+ Assert.That(handle, Is.Not.Null);
+ Assert.That(handle.Key, Is.EqualTo("test-key"));
+ }
+
+ [Test]
+ public async Task CleanupDuringAcquire_Should_NotCauseRaceCondition()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager(
+ cleanupInterval: TimeSpan.FromMilliseconds(50),
+ lockTimeout: TimeSpan.FromMilliseconds(100));
+
+ var tasks = new List();
+
+ // Act - 在清理过程中不断获取和释放锁
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(Task.Run(async () =>
+ {
+ for (var j = 0; j < 10; j++)
+ {
+ await using var handle = await manager.AcquireLockAsync($"key-{j % 5}");
+ await Task.Delay(10);
+ }
+ }));
+ }
+
+ // Assert
+ Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks));
+ }
+
+ [Test]
+ public void MultipleDispose_Should_BeSafe()
+ {
+ // Arrange
+ var manager = new AsyncKeyLockManager();
+
+ // Act
+ manager.Dispose();
+ manager.Dispose();
+
+ // Assert - 不应该抛出异常
+ Assert.Pass();
+ }
+
+ [Test]
+ public async Task HandleDispose_MultipleTimes_Should_BeSafe()
+ {
+ // Arrange
+ using var manager = new AsyncKeyLockManager();
+ var handle = await manager.AcquireLockAsync("test-key");
+
+ // Act
+ await handle.DisposeAsync();
+ await handle.DisposeAsync();
+ handle.Dispose();
+
+ // Assert - 不应该抛出异常
+ Assert.Pass();
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Concurrency/AsyncKeyLockManager.cs b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs
new file mode 100644
index 0000000..9ad2384
--- /dev/null
+++ b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs
@@ -0,0 +1,190 @@
+// Copyright (c) 2025 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System.Collections.Concurrent;
+using GFramework.Core.Abstractions.Concurrency;
+
+namespace GFramework.Core.Concurrency;
+
+///
+/// 工业级异步键锁管理器,支持自动清理、统计和调试
+///
+public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
+{
+ private readonly Timer _cleanupTimer;
+ private readonly ConcurrentDictionary _locks = new();
+ private readonly long _lockTimeoutMs;
+ private volatile bool _disposed;
+
+ // 统计计数器
+ private int _totalAcquired;
+ private int _totalCleaned;
+ private int _totalReleased;
+
+ ///
+ /// 初始化锁管理器
+ ///
+ /// 清理间隔,默认 60 秒
+ /// 锁超时时间,默认 300 秒
+ public AsyncKeyLockManager(TimeSpan? cleanupInterval = null, TimeSpan? lockTimeout = null)
+ {
+ var cleanupIntervalValue = cleanupInterval ?? TimeSpan.FromSeconds(60);
+ var lockTimeoutValue = lockTimeout ?? TimeSpan.FromSeconds(300);
+ _lockTimeoutMs = (long)lockTimeoutValue.TotalMilliseconds;
+
+ _cleanupTimer = new Timer(CleanupUnusedLocks, null, cleanupIntervalValue, cleanupIntervalValue);
+ }
+
+ ///
+ /// 异步获取指定键的锁
+ ///
+ public async ValueTask AcquireLockAsync(string key, CancellationToken cancellationToken = default)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ var entry = _locks.GetOrAdd(key, _ => new LockEntry());
+ Interlocked.Increment(ref entry.ReferenceCount);
+ entry.LastAccessTicks = System.Environment.TickCount64;
+
+ // 再次检查 disposed(防止在 GetOrAdd 后 Dispose)
+ if (_disposed)
+ {
+ Interlocked.Decrement(ref entry.ReferenceCount);
+ throw new ObjectDisposedException(nameof(AsyncKeyLockManager));
+ }
+
+ try
+ {
+ await entry.Semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+ Interlocked.Increment(ref _totalAcquired);
+ return new AsyncLockHandle(this, key, entry, System.Environment.TickCount64);
+ }
+ catch
+ {
+ // 如果等待失败(取消或异常),递减引用计数以防止泄漏
+ Interlocked.Decrement(ref entry.ReferenceCount);
+ throw;
+ }
+ }
+
+ ///
+ /// 同步获取指定键的锁(同步阻塞调用,优先使用 AcquireLockAsync)
+ ///
+ ///
+ /// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET)中导致死锁。
+ /// 仅在无法使用异步 API 时,作为低级逃生口(escape hatch)使用。
+ /// 如果可能,请优先使用 。
+ ///
+ public IAsyncLockHandle AcquireLock(string key)
+ {
+ // 使用 ConfigureAwait(false) 以避免在具有同步上下文的环境中捕获上下文,降低死锁风险
+ return AcquireLockAsync(key).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// 获取统计信息
+ ///
+ public LockStatistics GetStatistics()
+ {
+ return new LockStatistics
+ {
+ ActiveLockCount = _locks.Count,
+ TotalAcquired = _totalAcquired,
+ TotalReleased = _totalReleased,
+ TotalCleaned = _totalCleaned
+ };
+ }
+
+ ///
+ /// 获取活跃锁信息
+ ///
+ public IReadOnlyDictionary GetActiveLocks()
+ {
+ return _locks.ToDictionary(
+ kvp => kvp.Key,
+ kvp => new LockInfo
+ {
+ Key = kvp.Key,
+ ReferenceCount = kvp.Value.ReferenceCount,
+ LastAccessTicks = kvp.Value.LastAccessTicks,
+ // CurrentCount == 0 表示锁被持有,可能有等待者(近似值)
+ WaitingCount = kvp.Value.Semaphore.CurrentCount == 0 ? 1 : 0
+ });
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ _cleanupTimer.Dispose();
+
+ // 统一释放所有 semaphore
+ foreach (var entry in _locks.Values)
+ {
+ entry.Dispose();
+ }
+
+ _locks.Clear();
+ }
+
+ ///
+ /// 释放指定键的锁
+ ///
+ internal void ReleaseLock(string key, LockEntry entry)
+ {
+ entry.Semaphore.Release();
+ Interlocked.Decrement(ref entry.ReferenceCount);
+ Interlocked.Increment(ref _totalReleased);
+ entry.LastAccessTicks = System.Environment.TickCount64;
+ }
+
+ ///
+ /// 清理未使用的锁(不 Dispose semaphore,避免 race condition)
+ ///
+ private void CleanupUnusedLocks(object? state)
+ {
+ if (_disposed) return;
+
+ var now = System.Environment.TickCount64;
+
+ foreach (var (key, entry) in _locks)
+ {
+ // 只检查引用计数和超时,不 Dispose
+ if (entry.ReferenceCount == 0 &&
+ now - entry.LastAccessTicks > _lockTimeoutMs &&
+ _locks.TryRemove(key, out _))
+ {
+ Interlocked.Increment(ref _totalCleaned);
+ }
+ }
+ }
+
+ ///
+ /// 锁条目,包含信号量和引用计数
+ ///
+ internal sealed class LockEntry : IDisposable
+ {
+ public readonly SemaphoreSlim Semaphore = new(1, 1);
+ public long LastAccessTicks = System.Environment.TickCount64;
+ public int ReferenceCount;
+
+ public void Dispose()
+ {
+ Semaphore.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Concurrency/AsyncLockHandle.cs b/GFramework.Core/Concurrency/AsyncLockHandle.cs
new file mode 100644
index 0000000..d0e61bc
--- /dev/null
+++ b/GFramework.Core/Concurrency/AsyncLockHandle.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2025 GeWuYou
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using GFramework.Core.Abstractions.Concurrency;
+
+namespace GFramework.Core.Concurrency;
+
+///
+/// 异步锁句柄实现
+///
+internal sealed class AsyncLockHandle : IAsyncLockHandle
+{
+ private readonly AsyncKeyLockManager.LockEntry _entry;
+ private readonly string _key;
+ private readonly AsyncKeyLockManager _manager;
+ private int _disposed;
+
+ public AsyncLockHandle(AsyncKeyLockManager manager, string key, AsyncKeyLockManager.LockEntry entry,
+ long acquiredTicks)
+ {
+ _manager = manager;
+ _key = key;
+ _entry = entry;
+ Key = key;
+ AcquiredTicks = acquiredTicks;
+ }
+
+ public string Key { get; }
+ public long AcquiredTicks { get; }
+
+ public ValueTask DisposeAsync()
+ {
+ Dispose();
+ return ValueTask.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 0)
+ {
+ _manager.ReleaseLock(_key, _entry);
+ }
+ }
+}
\ No newline at end of file