You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 8中ChangeToken.OnChange引发栈溢出异常问题排查

PhysicalFileProvider监听文件变更时栈溢出问题排查

在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
);

额外优化点

  1. 删除SetUp方法中未使用的PhysicalFileProvider实例,清理冗余代码
  2. 使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 02:15:02