diff --git a/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs b/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs
index 720b212a..c36aba30 100644
--- a/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs
+++ b/GFramework.Core.Tests/Ioc/IocContainerLifetimeTests.cs
@@ -279,6 +279,36 @@ public class IocContainerLifetimeTests
}
}
+ [Test]
+ public void Dispose_Should_Only_Attempt_Lock_Disposal_Once_When_Called_Concurrently()
+ {
+ var container = new MicrosoftDiContainer();
+ var containerLock = GetContainerLock(container);
+ var releasedGate = false;
+
+ containerLock.EnterWriteLock();
+ try
+ {
+ var firstDisposeTask = Task.Run(container.Dispose);
+ Thread.Sleep(50);
+ var secondDisposeTask = Task.Run(container.Dispose);
+ Thread.Sleep(50);
+
+ containerLock.ExitWriteLock();
+ releasedGate = true;
+
+ Assert.That(async () => await Task.WhenAll(firstDisposeTask, secondDisposeTask).ConfigureAwait(false), Throws.Nothing);
+ Assert.That(GetLockDisposalStarted(container), Is.EqualTo(1));
+ }
+ finally
+ {
+ if (!releasedGate)
+ {
+ containerLock.ExitWriteLock();
+ }
+ }
+ }
+
///
/// 通过反射获取容器内部锁,用于构造可重复的并发释放竞态回归。
///
@@ -289,4 +319,15 @@ public class IocContainerLifetimeTests
Assert.That(lockField, Is.Not.Null);
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
}
+
+ ///
+ /// 读取锁销毁启动标记,验证并发释放路径不会重复执行底层锁销毁。
+ ///
+ private static int GetLockDisposalStarted(MicrosoftDiContainer container)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+ var flagField = typeof(MicrosoftDiContainer).GetField("_lockDisposalStarted", BindingFlags.NonPublic | BindingFlags.Instance);
+ Assert.That(flagField, Is.Not.Null);
+ return (int)flagField!.GetValue(container)!;
+ }
}
diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs
index aef15f04..ad362b76 100644
--- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs
+++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs
@@ -118,9 +118,16 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// 容器会先把 置为 并退出写锁,
/// 这样所有已在等待队列中的线程都能醒来并通过统一路径抛出容器级
/// 。只有当这些线程退场后,底层锁才可安全释放。
+ /// 该步骤只允许一个释放调用者执行,避免并发 重复销毁同一个
+ /// 并破坏幂等契约。
///
private void DisposeLockWhenQuiescent()
{
+ if (Interlocked.CompareExchange(ref _lockDisposalStarted, 1, 0) != 0)
+ {
+ return;
+ }
+
const int maxDisposeSpinAttempts = 512;
var spinWait = new SpinWait();
@@ -188,6 +195,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
///
private volatile bool _disposed;
+ ///
+ /// 标记底层读写锁的销毁流程是否已经启动,确保并发释放时最多只有一个线程尝试销毁锁实例。
+ ///
+ private int _lockDisposalStarted;
+
///
/// 读写锁,确保多线程环境下的线程安全操作
///
diff --git a/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md b/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md
index 0655b3be..22acd952 100644
--- a/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md
+++ b/ai-plan/public/microsoft-di-container-disposal/todos/microsoft-di-container-disposal-tracking.md
@@ -13,6 +13,7 @@
- `$gframework-pr-review` 已确认 latest-head review 仍存在 5 条 open AI thread,其中 `IIocContainer` 文档契约、`MicrosoftDiContainer.Clear()` 的不可达释放逻辑、`Dispose()` 并发竞态,以及 benchmark `Cleanup()` 缺乏异常隔离均已在本地补齐
- `CodeRabbit` 关于 `GFramework.Cqrs.Benchmarks` 的 cleanup 问题虽然标在单个文件上,但同类模式实际覆盖 `RequestBenchmarks`、`NotificationBenchmarks`、`RequestPipelineBenchmarks`、`RequestStartupBenchmarks`、`StreamingBenchmarks`、`RequestInvokerBenchmarks`、`StreamInvokerBenchmarks`,当前已通过共享 helper 一次性收敛
- `MicrosoftDiContainer.Dispose()` 现会先对外发布 `_disposed` 状态并释放写锁,让等待线程统一抛出容器级 `ObjectDisposedException`;随后仅在锁静默后才销毁底层 `ReaderWriterLockSlim`
+ - 针对剩余的 `greptile` P1,本轮进一步将底层锁销毁收敛为单次执行,避免两个并发 `Dispose()` 调用都进入 `DisposeLockWhenQuiescent()` 时触发双重 `ReaderWriterLockSlim.Dispose()`
## 当前活跃事实
@@ -42,7 +43,12 @@
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
+## 待补最新验证
+
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~IocContainerLifetimeTests"`
+- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+
## 下一推荐步骤
-1. 再次运行 `$gframework-pr-review` 或检查生成的 JSON,确认当前 latest-head open threads 是否只剩待推送的 GitHub 状态差异
-2. 关注 push 后若仍有 review thread 未关闭,优先核对其是否属于 stale comment 还是需要额外文档/测试补充
+1. 运行 `IocContainerLifetimeTests` 与 `GFramework.Core` Release build,确认单次锁销毁修复没有引入新的 warning 或回归
+2. 再次运行 `$gframework-pr-review` 或检查生成的 JSON,确认当前 latest-head open threads 是否只剩待推送的 GitHub 状态差异
diff --git a/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md b/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md
index b4a28617..d81d66e9 100644
--- a/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md
+++ b/ai-plan/public/microsoft-di-container-disposal/traces/microsoft-di-container-disposal-trace.md
@@ -27,3 +27,14 @@
### 当前下一步
1. 推送当前分支后重新运行 `$gframework-pr-review`,确认 latest-head open threads 是否已与本地修复对齐
+
+### 阶段:收敛剩余并发 Dispose 双重锁销毁竞态(MICROSOFT-DI-DISPOSAL-RP-001)
+
+- 根据用户补充的 `greptile` P1,重新核对 `MicrosoftDiContainer.Dispose()` 的尾部流程后确认还存在一个更窄的窗口:
+ - 线程 A 与线程 B 都可能通过最外层 `_disposed` 快速路径
+ - 线程 A 完成主释放并退出写锁后,线程 B 仍可能拿到写锁、因为 `_disposed == true` 直接返回,但 `finally` 仍会调用 `DisposeLockWhenQuiescent()`
+ - 这样两个线程都可能执行 `_lock.Dispose()`;第二次调用会抛出 `ObjectDisposedException`
+- 本轮修复决策:
+ - 在 `DisposeLockWhenQuiescent()` 入口增加 `Interlocked.CompareExchange` 守卫,把底层锁销毁流程收敛为单次执行
+ - 保持现有“先发布 `_disposed`、再等待 waiter 退场”的语义不变,只修复重复销毁底层锁的尾部竞态
+ - 在 `IocContainerLifetimeTests` 增加更直接的回归断言,验证并发 `Dispose()` 后锁销毁启动标记只会变为 `1`