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

ThreadPool.RegisterWaitForSingleObject引发RegisteredWaitHandle内存泄漏

WaitHandle异步扩展方法内存泄漏问题排查与解决

问题背景

我为WaitHandle(尤其是ManualResetEventSlim)编写了异步扩展方法WaitForSignalOrCancelAsync,用于异步场景。将代码部署为Windows服务运行数小时后,内存占用持续增长,CPU占用也有所上升。通过dotMemory分析发现,TaskCompletionSource<bool>、CancellationCallbackInfo、ThreadPoolWaitOrTimerCallback三类对象大量堆积,手动触发GC也无法回收。该扩展方法最初基于MSDN的"从等待句柄到TAP"示例实现,后续尝试了Ivan的解决方案及AsyncEx库,问题仍未解决;单元测试显示RegisteredWaitHandle等资源未完全释放。

核心原因

  • RegisteredWaitHandle未显式释放:使用ThreadPool.RegisterWaitForSingleObject注册等待后,若未主动调用Unregister(),会导致句柄及关联的回调对象(包括ThreadPoolWaitOrTimerCallback)无法被GC回收,进而牵连TaskCompletionSource和取消回调相关的CancellationCallbackInfo对象。
  • 取消回调的资源清理遗漏:仅在取消时完成TaskCompletionSource但未注销等待句柄,或注销逻辑存在分支遗漏,会导致资源长期滞留。
  • ManualResetEventSlim的特殊处理:ManualResetEventSlim有轻量级和内核句柄两种模式,手动实现异步等待时若未正确处理其内部句柄生命周期,也容易引发泄漏。

修复方案

1. 确保RegisteredWaitHandle和取消注册始终被清理

在扩展方法中,必须保证无论等待正常完成还是被取消,都执行资源清理操作。可以用finally块兜底,同时避免重复注销:

public static async Task<bool> WaitForSignalOrCancelAsync(this WaitHandle waitHandle, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
    RegisteredWaitHandle? registeredWait = null;
    CancellationTokenRegistration? cancelReg = null;

    try
    {
        // 注册等待句柄的回调
        registeredWait = ThreadPool.RegisterWaitForSingleObject(
            waitHandle,
            (state, timedOut) => ((TaskCompletionSource<bool>)state!).TrySetResult(!timedOut),
            tcs,
            Timeout.InfiniteTimeSpan,
            executeOnlyOnce: true);

        // 注册取消令牌的回调
        cancelReg = cancellationToken.Register(() =>
        {
            tcs.TrySetCanceled();
            registeredWait?.Unregister(null);
        });

        return await tcs.Task.ConfigureAwait(false);
    }
    finally
    {
        // 兜底清理:确保等待句柄注销,取消注册释放
        registeredWait?.Unregister(null);
        cancelReg?.Dispose();
    }
}

2. 优先使用.NET内置的WaitAsync方法

从.NET Core 2.1和.NET 5开始,ManualResetEventSlim已经内置了WaitAsync方法,官方实现已经处理了所有资源管理细节,完全可以替代自定义扩展:

// 直接使用官方异步等待方法,无需自己实现
await manualResetEventSlim.WaitAsync(cancellationToken).ConfigureAwait(false);

3. 完善资源释放的单元测试

编写单元测试时,要验证资源是否被正确释放。可以通过强制GC后检查对象存活情况,或借助内存分析工具确认:

[Test]
public async Task WaitForSignalOrCancelAsync_ShouldReleaseAllResources()
{
    using var mres = new ManualResetEventSlim(false);
    using var cts = new CancellationTokenSource();

    // 触发等待后立即取消
    var waitTask = mres.WaitForSignalOrCancelAsync(cts.Token);
    cts.Cancel();
    await waitTask;

    // 强制GC并等待终结器执行
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    // 此处可通过dotMemory快照对比,确认RegisteredWaitHandle等对象已被回收
}

关键注意事项

  • 使用TaskCreationOptions.RunContinuationsAsynchronously可以避免同步上下文死锁,同时减少回调线程的阻塞。
  • 长期运行的Windows服务中,非托管资源(如RegisteredWaitHandle)必须显式释放,GC无法自动回收这类资源。
  • 尽量避免手动实现TAP模式的等待句柄封装,优先使用官方提供的异步API,这类API经过充分测试,能避免大部分资源泄漏问题。

内容的提问来源于stack exchange,提问作者behindwhere

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 22:40:15