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..a748db3 --- /dev/null +++ b/GFramework.Core.Abstractions/Concurrency/LockStatistics.cs @@ -0,0 +1,66 @@ +// 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; } + + /// + /// 等待队列长度 + /// + 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..7872482 --- /dev/null +++ b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs @@ -0,0 +1,176 @@ +// 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 _lockTimeoutTicks; + 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); + _lockTimeoutTicks = (long)(lockTimeoutValue.TotalMilliseconds * TimeSpan.TicksPerMillisecond / 10000); + + _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)); + } + + await entry.Semaphore.WaitAsync(cancellationToken); + + Interlocked.Increment(ref _totalAcquired); + + return new AsyncLockHandle(this, key, entry, System.Environment.TickCount64); + } + + /// + /// 同步获取指定键的锁 + /// + public IAsyncLockHandle AcquireLock(string key) + { + return AcquireLockAsync(key).AsTask().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, + 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 > _lockTimeoutTicks && + _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