TPL Dataflow模拟身份运行管道时泄漏数百万句柄求助
我的WinForms应用在执行大型TPL Dataflow管道操作时频繁崩溃至桌面,常规try/catch无法捕获异常,仅在最外层Program.cs/Main()中添加try/catch后才捕获到:Unhandled Exception: System.Security.SecurityException: Insufficient system resources exist to complete the requested service.
注:问题发生在配备384GB内存和56个逻辑处理器的机器上。
通过Sysinternals Process Explorer监控句柄计数发现:仅在模拟身份时,进程会占用多达1400万个句柄;未模拟身份时,同一操作仅占用350-400个句柄。
复现步骤
只要拥有可用于模拟身份的备用账号即可轻松复现:
- 参考
WindowsIdentity.Impersonate的官方示例代码实现模拟身份的控制台应用 - 添加本文提供的空操作TPL Dataflow类
- 在模拟身份前实例化一个
Dummy对象,在模拟身份后再实例化另一个 - 在Process Explorer的句柄视图中可观察到模拟身份时大量句柄被创建
TPL Dataflow 6.0.0和7.0.0版本均存在此问题,求修复/解决方法?
Dummy类代码
public class Dummy { const int BATCH_SIZE = 1_048_576; public Dummy() { DataflowLinkOptions linkOptions = new DataflowLinkOptions() { PropagateCompletion = true }; TransformBlock<int, int> transformer = new TransformBlock<int, int>((int x) => { return x - 1000; }); BatchBlock<int> batcher = new BatchBlock<int>(BATCH_SIZE); ActionBlock<int[]> actioner = new ActionBlock<int[]>((int[] x) => { Console.WriteLine("done"); }); transformer.LinkTo(batcher, linkOptions); batcher.LinkTo(actioner, linkOptions); for (int i = 0; i < 100_000_000; i++) { transformer.Post(i); } transformer.Complete(); actioner.Completion.Wait(); Console.WriteLine("done"); } }
编辑1:问题不限于TPL Dataflow
@theodor-zoulias的假设正确,将Dummy构造函数改为以下代码后,模拟身份时进程仍会占用大量Token句柄:
Parallel.For(0 ,100_000 ,(int i) => { if (i % 1_000 == 0) { Console.WriteLine(i); } List<int> list = new List<int>(100_000); list.AddRange(Enumerable.Range(0, 100_000)); });
演示应用中可观察到该行为。
编辑2:提供复现代码
已上传完整的复现应用代码。
编辑3:根因分析
通过dnSpy调试发现,根本原因是.NET Framework每次运行Task时都会复制Token句柄,用户代码几乎无法直接控制,但可能存在未知的解决机制。调用栈如下:
[pinvokes kernel32.dll DuplicateHandle()] mscorlib.dll!System.Security.Principal.WindowsIdentity.CreateFromToken(System.IntPtr userToken) (IL=0x003D, Native=0x00007FF8D33DBE20+0x122) mscorlib.dll!System.Security.Principal.WindowsIdentity.WindowsIdentity(System.IntPtr userToken, string authType, int isAuthenticated) (IL=0x0042, Native=0x00007FF8D33DBCD0+0x10E) mscorlib.dll!System.Security.Principal.WindowsIdentity.WindowsIdentity(Microsoft.Win32.SafeHandles.SafeAccessTokenHandle safeTokenHandle) (IL=0x000E, Native=0x00007FF8D33DCFE0+0x62) mscorlib.dll!System.Security.SecurityContext.CreateCopy() (IL=0x0052, Native=0x00007FF8D33E1CD0+0x11E) mscorlib.dll!System.Threading.ExecutionContext.CreateCopy() (IL≈0x007D, Native=0x00007FF8D33E19A0+0x224) mscorlib.dll!System.Threading.Tasks.Task.CopyExecutionContext(System.Threading.ExecutionContext capturedContext) (IL≈0x0013, Native=0x00007FF8D33B5260+0x6A) mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) (IL≈0x00AA, Native=0x00007FF8D33B3320+0x284) mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) (IL=0x0063, Native=0x00007FF8D33B3000+0xF4) mscorlib.dll!System.Threading.Tasks.ThreadPoolTaskScheduler.TryExecuteTaskInline(System.Threading.Tasks.Task task, bool taskWasPreviouslyQueued) (IL≈0x000F, Native=0x00007FF8D33B2F00+0x78) mscorlib.dll!System.Threading.Tasks.TaskScheduler.TryRunInline(System.Threading.Tasks.Task task, bool taskWasPreviouslyQueued) (IL≈0x004C, Native=0x00007FF8D33B26E0+0x143) mscorlib.dll!System.Threading.Tasks.Task.InternalRunSynchronously(System.Threading.Tasks.TaskScheduler scheduler, bool waitForCompletion) (IL≈0x0083, Native=0x00007FF8D33B2160+0x247) mscorlib.dll!System.Threading.Tasks.Task.RunSynchronously(System.Threading.Tasks.TaskScheduler scheduler) (IL=0x0016, Native=0x00007FF8D33B20C0+0x85) mscorlib.dll!System.Threading.Tasks.Parallel.ForWorker<object>(int fromInclusive, int toExclusive, System.Threading.Tasks.ParallelOptions parallelOptions, System.Action<int> body, System.Action<int, System.Threading.Tasks.ParallelLoopState> bodyWithState, System.Func<int, System.Threading.Tasks.ParallelLoopState, object, object> bodyWithLocal, System.Func<object> localInit, System.Action<object> localFinally) (IL=0x01A9, Native=0x00007FF8D33A1B20+0x69F) mscorlib.dll!System.Threading.Tasks.Parallel.For(int fromInclusive, int toExclusive, System.Action<int> body) (IL=0x001F, Native=0x00007FF8D33A0280+0xEA) ImpersonationTests.exe!Dummy.Dummy() (IL=0x0032, Native=0x00007FF8D33A0190+0xCA)
编辑4:线程数说明
我的实际应用(而非演示应用)线程数稳定在180-200左右,可通过Process Explorer观察到句柄持续增长的情况。
编辑5:评论回应
能否提供示例的内存转储以查看泄漏来源?
已上传内存转储相关内容。
并行for循环是否能正常终止?
可以,复现应用中Parallel.For在模拟身份前后都能正常运行完成。
实际代码未正确使用模拟身份
无法发布完整的企业应用代码,核心代码如下:
private async void Run_Click(object sender, EventArgs e) { Tool t = new Tool(); using (Utils u = new Utils()) { try { u.BeginImpersonation(); await Task.Run(() => t.RunPipeline()); } finally { u.EndImpersonation(); } } }
这不是资源泄漏
请查看Process Explorer的屏幕录制,句柄数持续增长直至耗尽系统资源,属于明显的泄漏行为。
解决方案是为应用池使用AD账号
这是WinForms应用,不存在应用池。
可能代码运行在Linux上,需访问数据库或文件夹
我们使用Windows系统,模拟身份是为了让权限较低的用户访问数据库和UNC路径。目前我因自身权限足够,已注释掉模拟身份代码以正常运行应用。
内容的提问来源于stack exchange,提问作者amonroejj

