From ff1996e81bce2437d9a827f30cb41b935cffbdb7 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Tue, 21 Apr 2026 09:18:20 +0800
Subject: [PATCH] =?UTF-8?q?refactor(pause):=20=E6=94=B6=E5=8F=A3=20PauseSt?=
=?UTF-8?q?ackManager=20=E9=95=BF=E6=96=B9=E6=B3=95=E5=91=8A=E8=AD=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 重构 PauseStackManager 的销毁与 Pop 流程,拆分锁内状态迁移与锁外通知阶段
- 新增 PauseStackManager 销毁恢复通知回归测试,覆盖多暂停组销毁补发行为
- 更新 analyzer warning reduction 主题的 active tracking 与 trace,记录 RP-005 验证结果和下一恢复点
---
.../Pause/PauseStackManagerTests.cs | 31 +-
GFramework.Core/Pause/PauseStackManager.cs | 360 +++++++++++-------
.../analyzer-warning-reduction-tracking.md | 29 +-
.../analyzer-warning-reduction-trace.md | 23 ++
4 files changed, 299 insertions(+), 144 deletions(-)
diff --git a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs
index 4705357f..6e7b0581 100644
--- a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs
+++ b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs
@@ -394,6 +394,35 @@ public class PauseStackManagerTests
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False);
}
+ ///
+ /// 验证销毁时会向所有仍暂停的组补发恢复通知。
+ ///
+ [Test]
+ public async Task DestroyAsync_Should_NotifyResumedGroups()
+ {
+ var resumedGroups = new List();
+ var mockHandler = new MockPauseHandler();
+
+ _manager.RegisterHandler(mockHandler);
+ _manager.OnPauseStateChanged += (_, e) =>
+ {
+ if (!e.IsPaused)
+ {
+ resumedGroups.Add(e.Group);
+ }
+ };
+
+ _manager.Push("Global", PauseGroup.Global);
+ _manager.Push("Gameplay", PauseGroup.Gameplay);
+ mockHandler.Reset();
+
+ await _manager.DestroyAsync();
+
+ Assert.That(mockHandler.CallCount, Is.EqualTo(2));
+ Assert.That(mockHandler.LastIsPaused, Is.False);
+ Assert.That(resumedGroups, Is.EquivalentTo(new[] { PauseGroup.Global, PauseGroup.Gameplay }));
+ }
+
///
/// 验证并发Push是线程安全的
///
@@ -469,4 +498,4 @@ public class PauseStackManagerTests
LastIsPaused = null;
}
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Core/Pause/PauseStackManager.cs b/GFramework.Core/Pause/PauseStackManager.cs
index dd15fe3c..665c2d3a 100644
--- a/GFramework.Core/Pause/PauseStackManager.cs
+++ b/GFramework.Core/Pause/PauseStackManager.cs
@@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public ValueTask DestroyAsync()
{
if (_disposed)
+ {
return ValueTask.CompletedTask;
-
- List pausedGroups;
- IPauseHandler[] handlersSnapshot;
-
- _lock.EnterWriteLock();
- try
- {
- if (_disposed)
- return ValueTask.CompletedTask;
-
- _disposed = true;
-
- // 收集所有当前暂停的组
- pausedGroups = _pauseStacks
- .Where(kvp => kvp.Value.Count > 0)
- .Select(kvp => kvp.Key)
- .ToList();
-
- // 获取处理器快照
- handlersSnapshot = _handlers.ToArray();
-
- // 清理所有数据结构
- _pauseStacks.Clear();
- _tokenMap.Clear();
- _handlers.Clear();
-
- _logger.Debug("PauseStackManager destroyed");
- }
- finally
- {
- _lock.ExitWriteLock();
}
- // 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致
- foreach (var group in pausedGroups)
+ var destroySnapshot = TryBeginDestroy();
+ if (destroySnapshot == null)
{
- _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false");
-
- foreach (var handler in handlersSnapshot.OrderBy(h => h.Priority))
- {
- try
- {
- handler.OnPauseStateChanged(group, false);
- }
- catch (Exception ex)
- {
- _logger.Error($"Handler {handler.GetType().Name} failed during destruction", ex);
- }
- }
-
- // 触发事件
- try
- {
- RaisePauseStateChanged(group, false);
- }
- catch (Exception ex)
- {
- _logger.Error($"Event subscriber failed during destruction for group {group}", ex);
- }
+ return ValueTask.CompletedTask;
}
- // 释放锁资源
+ NotifyDestroyedGroups(destroySnapshot.Value);
_lock.Dispose();
return ValueTask.CompletedTask;
@@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
public bool Pop(PauseToken token)
{
if (!token.IsValid)
+ {
return false;
-
- bool found;
- bool shouldNotify = false;
- PauseGroup notifyGroup = PauseGroup.Global;
-
- _lock.EnterWriteLock();
- try
- {
- ThrowIfDisposed();
-
- if (!_tokenMap.TryGetValue(token.Id, out var entry))
- {
- _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
- return false;
- }
-
- var group = entry.Group;
- var stack = _pauseStacks[group];
- var wasPaused = stack.Count > 0;
-
- // 从栈中移除
- var tempStack = new Stack();
- found = false;
-
- while (stack.Count > 0)
- {
- var current = stack.Pop();
- if (current.TokenId == token.Id)
- {
- found = true;
- break;
- }
-
- tempStack.Push(current);
- }
-
- // 恢复栈结构
- while (tempStack.Count > 0)
- {
- stack.Push(tempStack.Pop());
- }
-
- if (found)
- {
- _tokenMap.Remove(token.Id);
- _logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})");
-
- // 状态变化检测:从暂停 → 未暂停
- if (wasPaused && stack.Count == 0)
- {
- shouldNotify = true;
- notifyGroup = group;
- }
- }
- }
- finally
- {
- _lock.ExitWriteLock();
}
- // 在锁外通知处理器,避免死锁
- if (shouldNotify)
+ var result = TryPopEntry(token);
+ if (result.ShouldNotify)
{
- NotifyHandlers(notifyGroup, false);
+ NotifyHandlers(result.NotifyGroup, false);
}
- return found;
+ return result.Found;
}
///
@@ -443,6 +334,200 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
}
+ ///
+ /// 采集销毁所需的快照并清空内部状态。
+ ///
+ ///
+ /// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 。
+ ///
+ ///
+ /// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行,
+ /// 以避免在生命周期结束阶段持锁调用用户代码。
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。
+ ///
+ /// 销毁阶段采集的分组与处理器快照。
+ 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);
+ }
+ }
+
+ ///
+ /// 在锁内执行令牌移除,并返回锁外通知所需的信息。
+ ///
+ /// 要移除的暂停令牌。
+ /// 包含本次弹出结果和后续通知决策的快照。
+ ///
+ /// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序,
+ /// 只在最后一个暂停请求被移除时触发恢复通知。
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。
+ ///
+ /// 要修改的暂停栈。
+ /// 目标令牌标识。
+ /// 如果找到了目标令牌则返回 。
+ private static bool RemoveEntryFromStack(Stack stack, Guid tokenId)
+ {
+ var tempStack = new Stack();
+ 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;
+ }
+
+ ///
+ /// 收集当前仍处于暂停状态的分组列表。
+ ///
+ /// 包含所有暂停中的分组的数组。
+ private PauseGroup[] CollectPausedGroups()
+ {
+ return _pauseStacks
+ .Where(kvp => kvp.Value.Count > 0)
+ .Select(kvp => kvp.Key)
+ .ToArray();
+ }
+
+ ///
+ /// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。
+ ///
+ /// 已按优先级排序的处理器快照。
+ private IPauseHandler[] CreateHandlerSnapshot()
+ {
+ return _handlers
+ .OrderBy(handler => handler.Priority)
+ .ToArray();
+ }
+
+ ///
+ /// 统一使用给定的处理器快照派发暂停状态变化通知。
+ ///
+ /// 发生状态变化的暂停组。
+ /// 新的暂停状态。
+ /// 要通知的处理器快照。
+ /// 是否处于销毁补发路径。
+ private void NotifyHandlersSnapshot(
+ PauseGroup group,
+ bool isPaused,
+ IReadOnlyList 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);
+ }
+ }
+ }
+
+ ///
+ /// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。
+ ///
+ /// 需要补发恢复事件的暂停组。
+ private void RaiseDestroyStateChanged(PauseGroup group)
+ {
+ try
+ {
+ RaisePauseStateChanged(group, false);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Event subscriber failed during destruction for group {group}", ex);
+ }
+ }
+
///
/// 内部查询暂停状态的方法,不加锁。
///
@@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
_lock.EnterReadLock();
try
{
- handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray();
+ handlersSnapshot = CreateHandlerSnapshot();
}
finally
{
@@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
// 在锁外遍历快照并通知处理器
- foreach (var handler in handlersSnapshot)
- {
- try
- {
- handler.OnPauseStateChanged(group, isPaused);
- }
- catch (Exception ex)
- {
- _logger.Error($"Handler {handler.GetType().Name} failed", ex);
- }
- }
+ NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false);
// 触发事件
RaisePauseStateChanged(group, isPaused);
@@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
protected override void OnInit()
{
}
-}
\ No newline at end of file
+
+ ///
+ /// 锁内采集的销毁快照,供锁外补发恢复通知使用。
+ ///
+ /// 销毁前仍处于暂停状态的分组。
+ /// 按优先级排序后的处理器快照。
+ private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot);
+
+ ///
+ /// Pop 操作的锁内结果快照。
+ ///
+ /// 是否成功移除了目标令牌。
+ /// 是否需要在锁外发出恢复通知。
+ /// 需要通知的暂停组。
+ private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup)
+ {
+ ///
+ /// 表示未找到目标令牌时的默认结果。
+ ///
+ public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global);
+ }
+}
diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md
index 4586470a..2e7b710f 100644
--- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md
+++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md
@@ -7,21 +7,22 @@
## 当前恢复点
-- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-004`
-- 当前阶段:`Phase 4`
+- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005`
+- 当前阶段:`Phase 5`
- 当前焦点:
- - 已完成当前分支 PR #263 的最新 review follow-up,本地确认并修复 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs`
- 的 `null!` 可空契约问题,同时消除 active trace 的重复标题 `MD024`
- - 已确认 PR 上的测试信号为 `2134 Passed / 0 Failed`;MegaLinter 唯一告警来自 CI 中 `dotnet-format` restore 失败,
- 当前本地 follow-up 不需要额外处理
- - 下一轮若继续推进,优先从 `PauseStackManager`、`CoroutineScheduler` 或 `Store` 的剩余 `MA0051` 中只选一个切入点
+ - 已完成 `GFramework.Core/Pause/PauseStackManager.cs` 的 `MA0051` 收口:将 `DestroyAsync` 与 `Pop` 拆分为锁内状态迁移、
+ 栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变
+ - 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知”
+ - 下一轮若继续推进,优先在 `CoroutineScheduler` 或 `Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的
+ `PauseStackManager`
## 当前状态摘要
- 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理
- 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险
-- 当前 `GFramework.Cqrs` 的剩余 warning 热点已从 active 入口移除;主题内剩余 warning 主要集中在 `GFramework.Core` 长方法、
- 文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题
+- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在
+ `GFramework.Core/Coroutine/CoroutineScheduler.cs`、`GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、
+ delegate 形状和少量公共集合抽象接口问题
## 当前活跃事实
@@ -30,6 +31,7 @@
- `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证
- `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证
- `RP-004` 已完成当前 PR review follow-up:修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题
+- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试
- 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射
## 当前风险
@@ -58,11 +60,16 @@
- `RP-004` 的定向验证结果:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=`
- 结果:`0 Warning(s)`,`0 Error(s)`
+- `RP-005` 的定向验证结果:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
+ - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表中
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
+ - 结果:`25 Passed`,`0 Failed`
- active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史
## 下一步
1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录
-2. 优先在 `GFramework.Core/Pause/PauseStackManager.cs`、`GFramework.Core/Coroutine/CoroutineScheduler.cs` 与
- `GFramework.Core/StateManagement/Store.cs` 的 `MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
+2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs` 与 `GFramework.Core/StateManagement/Store.cs`
+ 的 `MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面
3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/`
diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md
index 58f5485b..33fb83c8 100644
--- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md
+++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md
@@ -1,5 +1,28 @@
# Analyzer Warning Reduction 追踪
+## 2026-04-21 — RP-005
+
+### 阶段:PauseStackManager `MA0051` 收口(RP-005)
+
+- 按 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择
+ `GFramework.Core/Pause/PauseStackManager.cs`,因为该文件体量明显小于 `CoroutineScheduler` 和 `Store`,
+ 且已有稳定的 `PauseStackManagerTests` 覆盖暂停栈、跨组独立性、事件通知与并发 `Push/Pop` 行为
+- 先用 `warnings-only` 定向构建确认 `DestroyAsync` 与 `Pop` 仍分别命中 `MA0051`,再把逻辑拆分为:
+ - `TryBeginDestroy`
+ - `NotifyDestroyedGroups`
+ - `TryPopEntry`
+ - `RemoveEntryFromStack`
+- 额外抽出 `CreateHandlerSnapshot` 与 `NotifyHandlersSnapshot`,统一普通通知与销毁补发路径的处理器排序和异常日志,
+ 保持原有“锁内采集快照、锁外调用处理器与事件”的并发策略不变
+- 为销毁路径新增 `DestroyAsync_Should_NotifyResumedGroups`,验证当多个暂停组在销毁前仍为暂停态时,
+ 处理器和事件订阅者都会收到 `IsPaused=false` 的恢复信号
+- 验证通过:
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"`
+ - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=`
+ - 结果:`25 Passed`,`0 Failed`
+- 下一步保持原节奏:只在 `CoroutineScheduler` 或 `Store` 中二选一继续,不与其他 warning 家族混做
+
## 2026-04-21 — RP-003
### 阶段:Architecture 生命周期 `MA0051` 收口(RP-003)