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
相关产品推荐
相关产品推荐

