GFramework/GFramework.Core/Concurrency/AsyncKeyLockManager.cs
GeWuYou 378d7afb23 fix(concurrency): 修复异步锁管理器中的超时单位错误和资源泄漏问题
- 修正 _lockTimeoutTicks 为 _lockTimeoutMs,统一使用毫秒作为时间单位
- 在 AcquireLockAsync 方法中添加异常处理,防止锁等待失败时的资源泄漏
- 为同步 AcquireLock 方法添加 ConfigureAwait(false) 避免死锁风险
- 更新锁统计信息中的 WaitingCount 字段为近似值说明
- 修正清理逻辑中使用正确的超时单位进行比较
2026-03-11 19:17:03 +08:00

190 lines
6.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
/// <summary>
/// 工业级异步键锁管理器,支持自动清理、统计和调试
/// </summary>
public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
{
private readonly Timer _cleanupTimer;
private readonly ConcurrentDictionary<string, LockEntry> _locks = new();
private readonly long _lockTimeoutMs;
private volatile bool _disposed;
// 统计计数器
private int _totalAcquired;
private int _totalCleaned;
private int _totalReleased;
/// <summary>
/// 初始化锁管理器
/// </summary>
/// <param name="cleanupInterval">清理间隔,默认 60 秒</param>
/// <param name="lockTimeout">锁超时时间,默认 300 秒</param>
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);
}
/// <summary>
/// 异步获取指定键的锁
/// </summary>
public async ValueTask<IAsyncLockHandle> 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;
}
}
/// <summary>
/// 同步获取指定键的锁(同步阻塞调用,优先使用 AcquireLockAsync
/// </summary>
/// <remarks>
/// 此方法通过同步等待异步操作完成,可能在具有同步上下文的环境(例如 UI 线程、经典 ASP.NET中导致死锁。
/// 仅在无法使用异步 API 时作为低级逃生口escape hatch使用。
/// 如果可能,请优先使用 <see cref="AcquireLockAsync(string,System.Threading.CancellationToken)"/>。
/// </remarks>
public IAsyncLockHandle AcquireLock(string key)
{
// 使用 ConfigureAwait(false) 以避免在具有同步上下文的环境中捕获上下文,降低死锁风险
return AcquireLockAsync(key).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
}
/// <summary>
/// 获取统计信息
/// </summary>
public LockStatistics GetStatistics()
{
return new LockStatistics
{
ActiveLockCount = _locks.Count,
TotalAcquired = _totalAcquired,
TotalReleased = _totalReleased,
TotalCleaned = _totalCleaned
};
}
/// <summary>
/// 获取活跃锁信息
/// </summary>
public IReadOnlyDictionary<string, LockInfo> 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
});
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_cleanupTimer.Dispose();
// 统一释放所有 semaphore
foreach (var entry in _locks.Values)
{
entry.Dispose();
}
_locks.Clear();
}
/// <summary>
/// 释放指定键的锁
/// </summary>
internal void ReleaseLock(string key, LockEntry entry)
{
entry.Semaphore.Release();
Interlocked.Decrement(ref entry.ReferenceCount);
Interlocked.Increment(ref _totalReleased);
entry.LastAccessTicks = System.Environment.TickCount64;
}
/// <summary>
/// 清理未使用的锁(不 Dispose semaphore避免 race condition
/// </summary>
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);
}
}
}
/// <summary>
/// 锁条目,包含信号量和引用计数
/// </summary>
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();
}
}
}