.NET Core进程读取子进程输出死锁,如何用WinDBG排查?
排查.NET跨进程I/O死锁(stdout/stderr缓冲区阻塞导致)
问题背景
遇到了微软文档中提到的父进程读取子进程stdout和stderr时的死锁场景,自身无法复现但同事可以,增加了排查难度。
死锁发生代码
死锁出现在以下代码的process.StandardOutput.ReadLineAsync方法中:
public async Task<ProcessRunnerResult> Run(string executablePath, IProcessRunner.Mode mode, string? args = null, string? workingDirectory = null, Action<ProcessStartInfo>? beforeInvoke = null, Action<string>? notifyStdOutLine = null, CancellationToken cancellationToken = default) { var process = new Process { StartInfo = new ProcessStartInfo { FileName = executablePath, Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, UseShellExecute = false, WorkingDirectory = workingDirectory } }; string? stdErr = null; try { beforeInvoke?.Invoke(process.StartInfo); process.Start(); string? line; while ((line = await process.StandardOutput.ReadLineAsync(cancellationToken)) != null) { notifyStdOutLine?.Invoke(line); } stdErr = await process.StandardError.ReadToEndAsync(cancellationToken); } catch (Exception ex) { if (mode == IProcessRunner.Mode.NeverThrow) { return new ProcessRunnerResult(-1, null, ExceptionDispatchInfo.Capture(ex)); } throw new ProcessRunnerException(executablePath, args, -1, ex); } if (mode == IProcessRunner.Mode.RethrowAndIfNonZeroExitCode && process.ExitCode != 0) { throw new ProcessRunnerException(executablePath, args, process.ExitCode); } return new ProcessRunnerResult(process.ExitCode, stdErr); }
死锁原因推测
父进程在循环读取stdout的过程中阻塞,而子进程向stderr输出的内容填满了缓冲区,子进程因无法继续写入stderr而阻塞,最终导致双向死锁:父进程等待子进程输出stdout,子进程等待父进程读取stderr。
利用WinDBG和完整内存Dump排查步骤
此类死锁属于跨进程I/O阻塞死锁,并非托管代码内的锁竞争,因此!sos.syncblk无法捕获有效信息,且sosex不支持.NET Core,需通过以下步骤分析父、子进程的Dump:
1. 父进程Dump分析
- 定位阻塞线程:执行
!clrstack -all查看所有线程的托管调用栈,找到卡在Process.StandardOutput.ReadLineAsync的线程,确认该线程处于等待状态。 - 分析异步任务:执行
!dumpasync列出所有未完成的异步任务,定位到与stdout/stderr读取相关的任务,查看任务的等待来源与状态。 - 检查Process实例状态:
- 执行
!dumpheap -type System.Diagnostics.Process列出所有Process对象实例; - 对目标实例执行
!do <实例地址>,查看其StandardOutput、StandardError对应的流对象状态,确认是否存在未完成的阻塞读取操作。
- 执行
- 查看原生调用栈:执行
k(原生栈)或!dumpstack命令,确认线程是否卡在Win32的ReadFile等读取API上,验证是在等待子进程的输出数据。
2. 子进程Dump分析
- 定位输出阻塞线程:执行
!threads或k命令,找到负责向stderr输出内容的线程,查看其是否卡在WriteFile等输出API上,确认是因缓冲区满导致阻塞。 - 检查stderr缓冲区状态:
- 执行
!handle 0 f列出所有进程句柄,找到对应stderr的句柄; - 执行
!handle <stderr句柄> f查看句柄的详细信息,确认缓冲区已被填满。
- 执行
补充说明
已提供问题复现案例:master分支为存在死锁的代码,fix分支为修复后的版本。
内容的提问来源于stack exchange,提问作者mark
相关产品推荐
相关产品推荐

