.NET 8中ChangeToken.OnChange引发栈溢出异常问题排查
在Ubuntu 24.04环境下使用.NET 8编写的文件变更监听测试,执行dotnet test时触发栈溢出异常,但相同代码在其他项目中可正常运行;注释掉ChangeToken.OnChange代码块后,测试仅因断言失败不通过,但不会崩溃。
测试代码
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; using Shouldly; [TestFixture] public class PhysicalFileProviderOnChangeTests { private string _tempDirectory; private string _testFilePath; [SetUp] public void SetUp() { _tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(_tempDirectory); _testFilePath = Path.Combine(_tempDirectory, "testfile.txt"); File.WriteAllText(_testFilePath, "Hello, World!"); var provider = new PhysicalFileProvider(_tempDirectory); } [Test] public async Task TestOnChange_Handler() { var provider = new PhysicalFileProvider(_tempDirectory); var changeToken = provider.Watch("testfile.txt"); var changeDetected = false; ChangeToken.OnChange( () => changeToken, () => changeDetected = true ); File.WriteAllText(_testFilePath, "New Content"); await Task.Delay(100); changeDetected.ShouldBeTrue(); } [TearDown] public void TearDown() { if (Directory.Exists(_tempDirectory)) { Directory.Delete(_tempDirectory, true); } } }
栈溢出异常信息
The active test run was aborted. Reason: Test host process crashed : Stack overflow.
Repeat 10915 times:at System.Threading.CancellationTokenSource.Register(System.Delegate, System.Object, System.Threading.SynchronizationContext, System.Threading.ExecutionContext)
at System.Threading.CancellationToken.Register(System.Delegate, System.Object, Boolean, Boolean)
at Microsoft.Extensions.Internal.ChangeCallbackRegistrar.UnsafeRegisterChangeCallback[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.Action1<System.Object>, System.Object, System.Threading.CancellationToken, System.Action1<System.__Canon>, System.__Canon)
at Microsoft.Extensions.Primitives.CancellationChangeToken.RegisterChangeCallback(System.Action1<System.Object>, System.Object) at Microsoft.Extensions.Primitives.ChangeToken+ChangeTokenRegistration1[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].RegisterChangeTokenCallback(Microsoft.Extensions.Primitives.IChangeToken)
at Microsoft.Extensions.Primitives.ChangeToken+ChangeTokenRegistration1[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].OnChangeTokenFired() at Microsoft.Extensions.Primitives.ChangeToken+ChangeTokenRegistration1+<>c[[System.__Canon, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].b__7_0(System.Object)
at System.Threading.CancellationTokenSource.Invoke(System.Delegate, System.Object, System.Threading.CancellationTokenSource)at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean)
at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher+<>c.<.cctor>b__43_0(System.Object)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread)
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
异常触发特征
- 执行
dotnet test直接触发栈溢出,导致测试进程崩溃 - 相同代码在其他项目中可正常执行
- 注释掉以下代码块后,测试仅断言失败但无崩溃:
ChangeToken.OnChange( () => changeToken, () => changeDetected = true );
问题原因与修复方案
核心原因
ChangeToken.OnChange的第一个参数是返回新变更令牌的工厂委托,当前代码直接返回同一个changeToken实例。当文件变更触发回调后,ChangeToken.OnChange会尝试通过工厂委托获取新令牌重新注册监听,但因始终复用旧令牌,导致无限循环注册回调,最终引发栈溢出。
修复方案
修改工厂委托,每次调用时从PhysicalFileProvider重新获取新的变更令牌:
ChangeToken.OnChange( () => provider.Watch("testfile.txt"), // 每次重新获取新令牌 () => changeDetected = true );
额外优化点
- 删除
SetUp方法中未使用的PhysicalFileProvider实例,清理冗余代码 - 使用
using语句确保PhysicalFileProvider被正确释放,避免资源泄漏
修改后的完整测试代码:
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; using Shouldly; [TestFixture] public class PhysicalFileProviderOnChangeTests { private string _tempDirectory; private string _testFilePath; [SetUp] public void SetUp() { _tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(_tempDirectory); _testFilePath = Path.Combine(_tempDirectory, "testfile.txt"); File.WriteAllText(_testFilePath, "Hello, World!"); } [Test] public async Task TestOnChange_Handler() { using var provider = new PhysicalFileProvider(_tempDirectory); var changeDetected = false; ChangeToken.OnChange( () => provider.Watch("testfile.txt"), () => changeDetected = true ); File.WriteAllText(_testFilePath, "New Content"); await Task.Delay(100); changeDetected.ShouldBeTrue(); } [TearDown] public void TearDown() { if (Directory.Exists(_tempDirectory)) { Directory.Delete(_tempDirectory, true); } } }
内容的提问来源于stack exchange,提问作者Alexander Zeitler

