GFramework/GFramework.Core/Pause/PauseStackManager.cs
GeWuYou ff1996e81b refactor(pause): 收口 PauseStackManager 长方法告警
- 重构 PauseStackManager 的销毁与 Pop 流程,拆分锁内状态迁移与锁外通知阶段
- 新增 PauseStackManager 销毁恢复通知回归测试,覆盖多暂停组销毁补发行为
- 更新 analyzer warning reduction 主题的 active tracking 与 trace,记录 RP-005 验证结果和下一恢复点
2026-04-21 09:18:20 +08:00

608 lines
19 KiB
C#
Raw 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.

using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Pause;
using GFramework.Core.Logging;
using GFramework.Core.Utility;
namespace GFramework.Core.Pause;
/// <summary>
/// 暂停栈管理器实现,用于管理游戏中的暂停状态。
/// 支持多组暂停、嵌套暂停、以及暂停状态的通知机制。
/// </summary>
public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAsyncDestroyable
{
private readonly List<IPauseHandler> _handlers = new();
private readonly ReaderWriterLockSlim _lock = new();
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager));
private readonly Dictionary<PauseGroup, Stack<PauseEntry>> _pauseStacks = new();
private readonly Dictionary<Guid, PauseEntry> _tokenMap = new();
private volatile bool _disposed;
/// <summary>
/// 异步销毁方法,在组件销毁时调用。
/// </summary>
/// <returns>表示异步操作完成的任务。</returns>
public ValueTask DestroyAsync()
{
if (_disposed)
{
return ValueTask.CompletedTask;
}
var destroySnapshot = TryBeginDestroy();
if (destroySnapshot == null)
{
return ValueTask.CompletedTask;
}
NotifyDestroyedGroups(destroySnapshot.Value);
_lock.Dispose();
return ValueTask.CompletedTask;
}
/// <summary>
/// 暂停状态变化事件,当暂停状态发生改变时触发。
/// </summary>
public event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
/// <summary>
/// 推入一个新的暂停请求到指定的暂停组中。
/// </summary>
/// <param name="reason">暂停的原因描述。</param>
/// <param name="group">暂停组,默认为全局暂停组。</param>
/// <returns>表示此次暂停请求的令牌。</returns>
public PauseToken Push(string reason, PauseGroup group = PauseGroup.Global)
{
PauseToken token;
bool shouldNotify = false;
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
var wasPaused = IsPausedInternal(group);
var entry = new PauseEntry
{
TokenId = Guid.NewGuid(),
Reason = reason,
Group = group,
Timestamp = DateTime.UtcNow
};
if (!_pauseStacks.TryGetValue(group, out var stack))
{
stack = new Stack<PauseEntry>();
_pauseStacks[group] = stack;
}
stack.Push(entry);
_tokenMap[entry.TokenId] = entry;
_logger.Debug($"Pause pushed: {reason} (Group: {group}, Depth: {stack.Count})");
token = new PauseToken(entry.TokenId);
// 状态变化检测:从未暂停 → 暂停
shouldNotify = !wasPaused;
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知处理器,避免死锁
if (shouldNotify)
{
NotifyHandlers(group, true);
}
return token;
}
/// <summary>
/// 弹出指定的暂停请求。
/// </summary>
/// <param name="token">要弹出的暂停令牌。</param>
/// <returns>如果成功弹出则返回true否则返回false。</returns>
public bool Pop(PauseToken token)
{
if (!token.IsValid)
{
return false;
}
var result = TryPopEntry(token);
if (result.ShouldNotify)
{
NotifyHandlers(result.NotifyGroup, false);
}
return result.Found;
}
/// <summary>
/// 查询指定暂停组当前是否处于暂停状态。
/// </summary>
/// <param name="group">要查询的暂停组,默认为全局暂停组。</param>
/// <returns>如果该组处于暂停状态则返回true否则返回false。</returns>
public bool IsPaused(PauseGroup group = PauseGroup.Global)
{
_lock.EnterReadLock();
try
{
ThrowIfDisposed();
return IsPausedInternal(group);
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// 获取指定暂停组的暂停深度(即嵌套暂停的层数)。
/// </summary>
/// <param name="group">要查询的暂停组,默认为全局暂停组。</param>
/// <returns>暂停深度0表示未暂停。</returns>
public int GetPauseDepth(PauseGroup group = PauseGroup.Global)
{
_lock.EnterReadLock();
try
{
ThrowIfDisposed();
return _pauseStacks.TryGetValue(group, out var stack) ? stack.Count : 0;
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// 获取指定暂停组的所有暂停原因。
/// </summary>
/// <param name="group">要查询的暂停组,默认为全局暂停组。</param>
/// <returns>包含所有暂停原因的只读列表。</returns>
public IReadOnlyList<string> GetPauseReasons(PauseGroup group = PauseGroup.Global)
{
_lock.EnterReadLock();
try
{
ThrowIfDisposed();
if (!_pauseStacks.TryGetValue(group, out var stack))
return Array.Empty<string>();
return stack.Select(e => e.Reason).ToList();
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// 创建一个暂停作用域,支持 using 语法自动管理暂停生命周期。
/// </summary>
/// <param name="reason">暂停的原因描述。</param>
/// <param name="group">暂停组,默认为全局暂停组。</param>
/// <returns>表示暂停作用域的 IDisposable 对象。</returns>
public IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global)
{
if (_disposed)
throw new ObjectDisposedException(nameof(PauseStackManager),
"Cannot use PauseStackManager after it has been destroyed");
return new PauseScope(this, reason, group);
}
/// <summary>
/// 清空指定暂停组的所有暂停请求。
/// </summary>
/// <param name="group">要清空的暂停组。</param>
public void ClearGroup(PauseGroup group)
{
bool shouldNotify = false;
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_pauseStacks.TryGetValue(group, out var stack))
return;
var wasPaused = stack.Count > 0;
// 移除所有令牌
foreach (var entry in stack)
{
_tokenMap.Remove(entry.TokenId);
}
stack.Clear();
_logger.Warn($"Cleared all pauses for group: {group}");
// 状态变化检测
shouldNotify = wasPaused;
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知处理器,避免死锁
if (shouldNotify)
{
NotifyHandlers(group, false);
}
}
/// <summary>
/// 清空所有暂停组的所有暂停请求。
/// </summary>
public void ClearAll()
{
List<PauseGroup> pausedGroups;
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
pausedGroups = _pauseStacks
.Where(kvp => kvp.Value.Count > 0)
.Select(kvp => kvp.Key)
.ToList();
_pauseStacks.Clear();
_tokenMap.Clear();
_logger.Warn("Cleared all pauses for all groups");
}
finally
{
_lock.ExitWriteLock();
}
// 在锁外通知所有之前暂停的组,避免死锁
foreach (var group in pausedGroups)
{
NotifyHandlers(group, false);
}
}
/// <summary>
/// 注册一个暂停处理器,用于监听暂停状态的变化。
/// </summary>
/// <param name="handler">要注册的暂停处理器。</param>
public void RegisterHandler(IPauseHandler handler)
{
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_handlers.Contains(handler))
{
_handlers.Add(handler);
_logger.Debug($"Registered pause handler: {handler.GetType().Name}");
}
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 注销一个已注册的暂停处理器。
/// </summary>
/// <param name="handler">要注销的暂停处理器。</param>
public void UnregisterHandler(IPauseHandler handler)
{
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (_handlers.Remove(handler))
{
_logger.Debug($"Unregistered pause handler: {handler.GetType().Name}");
}
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 检查是否已销毁,如果已销毁则抛出异常
/// </summary>
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(PauseStackManager),
"Cannot use PauseStackManager after it has been destroyed");
}
}
/// <summary>
/// 采集销毁所需的快照并清空内部状态。
/// </summary>
/// <returns>
/// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 <see langword="null" />。
/// </returns>
/// <remarks>
/// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行,
/// 以避免在生命周期结束阶段持锁调用用户代码。
/// </remarks>
private DestroySnapshot? TryBeginDestroy()
{
_lock.EnterWriteLock();
try
{
if (_disposed)
{
return null;
}
_disposed = true;
var pausedGroups = CollectPausedGroups();
var handlersSnapshot = CreateHandlerSnapshot();
_pauseStacks.Clear();
_tokenMap.Clear();
_handlers.Clear();
_logger.Debug("PauseStackManager destroyed");
return new DestroySnapshot(pausedGroups, handlersSnapshot);
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。
/// </summary>
/// <param name="destroySnapshot">销毁阶段采集的分组与处理器快照。</param>
private void NotifyDestroyedGroups(DestroySnapshot destroySnapshot)
{
foreach (var group in destroySnapshot.PausedGroups)
{
_logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
NotifyHandlersSnapshot(group, false, destroySnapshot.HandlersSnapshot, isDestroying: true);
RaiseDestroyStateChanged(group);
}
}
/// <summary>
/// 在锁内执行令牌移除,并返回锁外通知所需的信息。
/// </summary>
/// <param name="token">要移除的暂停令牌。</param>
/// <returns>包含本次弹出结果和后续通知决策的快照。</returns>
/// <remarks>
/// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序,
/// 只在最后一个暂停请求被移除时触发恢复通知。
/// </remarks>
private PopResult TryPopEntry(PauseToken token)
{
_lock.EnterWriteLock();
try
{
ThrowIfDisposed();
if (!_tokenMap.TryGetValue(token.Id, out var entry))
{
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
return PopResult.NotFound;
}
var stack = _pauseStacks[entry.Group];
var wasPaused = stack.Count > 0;
var found = RemoveEntryFromStack(stack, token.Id);
if (!found)
{
return PopResult.NotFound;
}
_tokenMap.Remove(token.Id);
_logger.Debug($"Pause popped: {entry.Reason} (Group: {entry.Group}, Remaining: {stack.Count})");
return new PopResult(true, wasPaused && stack.Count == 0, entry.Group);
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。
/// </summary>
/// <param name="stack">要修改的暂停栈。</param>
/// <param name="tokenId">目标令牌标识。</param>
/// <returns>如果找到了目标令牌则返回 <see langword="true" />。</returns>
private static bool RemoveEntryFromStack(Stack<PauseEntry> stack, Guid tokenId)
{
var tempStack = new Stack<PauseEntry>();
var found = false;
while (stack.Count > 0)
{
var current = stack.Pop();
if (current.TokenId == tokenId)
{
found = true;
break;
}
tempStack.Push(current);
}
while (tempStack.Count > 0)
{
stack.Push(tempStack.Pop());
}
return found;
}
/// <summary>
/// 收集当前仍处于暂停状态的分组列表。
/// </summary>
/// <returns>包含所有暂停中的分组的数组。</returns>
private PauseGroup[] CollectPausedGroups()
{
return _pauseStacks
.Where(kvp => kvp.Value.Count > 0)
.Select(kvp => kvp.Key)
.ToArray();
}
/// <summary>
/// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。
/// </summary>
/// <returns>已按优先级排序的处理器快照。</returns>
private IPauseHandler[] CreateHandlerSnapshot()
{
return _handlers
.OrderBy(handler => handler.Priority)
.ToArray();
}
/// <summary>
/// 统一使用给定的处理器快照派发暂停状态变化通知。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">新的暂停状态。</param>
/// <param name="handlersSnapshot">要通知的处理器快照。</param>
/// <param name="isDestroying">是否处于销毁补发路径。</param>
private void NotifyHandlersSnapshot(
PauseGroup group,
bool isPaused,
IReadOnlyList<IPauseHandler> handlersSnapshot,
bool isDestroying)
{
foreach (var handler in handlersSnapshot)
{
try
{
handler.OnPauseStateChanged(group, isPaused);
}
catch (Exception ex)
{
var message = isDestroying
? $"Handler {handler.GetType().Name} failed during destruction"
: $"Handler {handler.GetType().Name} failed";
_logger.Error(message, ex);
}
}
}
/// <summary>
/// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。
/// </summary>
/// <param name="group">需要补发恢复事件的暂停组。</param>
private void RaiseDestroyStateChanged(PauseGroup group)
{
try
{
RaisePauseStateChanged(group, false);
}
catch (Exception ex)
{
_logger.Error($"Event subscriber failed during destruction for group {group}", ex);
}
}
/// <summary>
/// 内部查询暂停状态的方法,不加锁。
/// </summary>
/// <param name="group">要查询的暂停组。</param>
/// <returns>如果该组处于暂停状态则返回true否则返回false。</returns>
private bool IsPausedInternal(PauseGroup group)
{
return _pauseStacks.TryGetValue(group, out var stack) && stack.Count > 0;
}
/// <summary>
/// 通知所有已注册的处理器和事件订阅者暂停状态的变化。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">新的暂停状态。</param>
private void NotifyHandlers(PauseGroup group, bool isPaused)
{
_logger.Debug($"Notifying handlers: Group={group}, IsPaused={isPaused}");
// 在锁内获取处理器快照,避免并发修改异常
IPauseHandler[] handlersSnapshot;
_lock.EnterReadLock();
try
{
handlersSnapshot = CreateHandlerSnapshot();
}
finally
{
_lock.ExitReadLock();
}
// 在锁外遍历快照并通知处理器
NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false);
// 触发事件
RaisePauseStateChanged(group, isPaused);
}
/// <summary>
/// 以标准事件模式发布暂停状态变化事件。
/// 所有状态变更路径都通过该方法创建统一的事件参数,避免不同调用点出现不一致的载荷。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">暂停组变化后的新状态。</param>
private void RaisePauseStateChanged(PauseGroup group, bool isPaused)
{
OnPauseStateChanged?.Invoke(this, new PauseStateChangedEventArgs(group, isPaused));
}
/// <summary>
/// 初始化方法,在组件初始化时调用。
/// </summary>
protected override void OnInit()
{
}
/// <summary>
/// 锁内采集的销毁快照,供锁外补发恢复通知使用。
/// </summary>
/// <param name="PausedGroups">销毁前仍处于暂停状态的分组。</param>
/// <param name="HandlersSnapshot">按优先级排序后的处理器快照。</param>
private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot);
/// <summary>
/// Pop 操作的锁内结果快照。
/// </summary>
/// <param name="Found">是否成功移除了目标令牌。</param>
/// <param name="ShouldNotify">是否需要在锁外发出恢复通知。</param>
/// <param name="NotifyGroup">需要通知的暂停组。</param>
private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup)
{
/// <summary>
/// 表示未找到目标令牌时的默认结果。
/// </summary>
public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global);
}
}