子进程输出重定向时出现死锁问题排查(已遵循推荐模式)
子进程ProcessRunner类周期性死锁排查求助
我实现了一个用于生成子进程的ProcessRunner类,最初采用异步IO方式并发读取标准输出(stdout)与标准错误(stderr)流,理论上不会因输出重定向导致死锁,但在部分机器上仍周期性出现死锁现象。
随后我将代码修改为基于事件驱动的读取方式,然而死锁问题依旧周期性发生。
经排查,死锁发生时并未传入Action<Process>类型的回调参数,且已通过using关键字正确释放Process对象,但问题仍未解决。现寻求定位代码中的问题。
异步IO实现版本代码
public record ProcessRunnerResult(int ExitCode, string? StdErr, string? StdOut, ExceptionDispatchInfo? ExceptionDispatchInfo = null); public class ProcessRunnerException(string executablePath, string? args, int exitCode, string? stdErr = null, string? stdOut = null, Exception? innerException = null) : Exception($"Running << {executablePath} {args} >> failed with exit code {exitCode}", innerException) { public string? StdOut { get; } = stdOut; public string? StdErr { get; } = stdErr; public int ExitCode { get; } = exitCode; } public class ProcessRunner { public enum Mode { NeverThrow, RethrowReturnExitCode, RethrowAndIfNonZeroExitCode, RethrowIfNotCancelledReturnExitCode } public async Task<ProcessRunnerResult> Run(string executablePath, Mode mode, string? args = null, string? workingDirectory = null, Action<ProcessStartInfo>? beforeInvoke = null, Action<Process>? afterInvoke = null, Action<Process>? onCancelInvoke = null, Action<string>? notifyStdOutLine = null, Action<string>? notifyStdErrLine = null, CancellationToken cancellationToken = default) { using var process = new Process { StartInfo = new ProcessStartInfo { FileName = executablePath, Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, UseShellExecute = false, WorkingDirectory = workingDirectory } }; StringBuilder stdOutBuilder = new(); StringBuilder stdErrBuilder = new(); try { beforeInvoke?.Invoke(process.StartInfo); process.Start(); afterInvoke?.Invoke(process); Task[] tasks = [ ReadStreamAsync(notifyStdOutLine, stdOutBuilder, process.StandardOutput, cancellationToken), ReadStreamAsync(notifyStdErrLine, stdErrBuilder, process.StandardError, cancellationToken), ]; using (cancellationToken.Register(() => { onCancelInvoke?.Invoke(process); })) { await Task.WhenAll(tasks).ConfigureAwait(false); // Can stdout and stderr be closed by the child process before exiting? Who knows, but let us make sure the process is done. await process.WaitForExitAsync(cancellationToken); } } catch (OperationCanceledException) when (mode == Mode.RethrowIfNotCancelledReturnExitCode) { return new ProcessRunnerResult(-1, stdErrBuilder.ToString(), stdOutBuilder.ToString()); } catch (Exception ex) when (mode == Mode.NeverThrow) { return new ProcessRunnerResult(-1, null, null, ExceptionDispatchInfo.Capture(ex)); } catch (Exception ex) { throw new ProcessRunnerException(executablePath, args, -1, stdErrBuilder.ToString(), stdOutBuilder.ToString(), ex); } if (mode == Mode.RethrowAndIfNonZeroExitCode && process.ExitCode != 0) { throw new ProcessRunnerException(executablePath, args, process.ExitCode, stdErrBuilder.ToString(), stdOutBuilder.ToString()); } return new ProcessRunnerResult(process.ExitCode, stdErrBuilder.ToString(), stdOutBuilder.ToString()); } private static async Task ReadStreamAsync(Action<string>? notifyLine, StringBuilder builder, StreamReader reader, CancellationToken cancellationToken) { string? line; while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null) { builder.AppendLine(line); notifyLine?.Invoke(line); } } }
事件驱动实现版本代码
public class ProcessRunner { ... public async Task<ProcessRunnerResult> Run(string executablePath, Mode mode, string? args = null, string? workingDirectory = null, Action<ProcessStartInfo>? beforeInvoke = null, Action<Process>? afterInvoke = null, Action<Process>? onCancelInvoke = null, Action<string>? notifyStdOutLine = null, Action<string>? notifyStdErrLine = null, CancellationToken cancellationToken = default) { using var process = new Process { StartInfo = new ProcessStartInfo { FileName = executablePath, Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, UseShellExecute = false, WorkingDirectory = workingDirectory } }; StringBuilder sbStdOut = new(); StringBuilder sbStdErr = new(); try { beforeInvoke?.Invoke(process.StartInfo); process.OutputDataReceived += (_, args) => Process_OutputDataReceived(args.Data, notifyStdOutLine, sbStdOut); process.ErrorDataReceived += (_, args) => Process_OutputDataReceived(args.Data, notifyStdErrLine, sbStdErr); process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); afterInvoke?.Invoke(process); using (cancellationToken.Register(() => { onCancelInvoke?.Invoke(process); })) { await process.WaitForExitAsync(cancellationToken); } } catch (OperationCanceledException) when (mode == Mode.RethrowIfNotCancelledReturnExitCode) { return new ProcessRunnerResult(-1, sbStdErr.ToString(), sbStdOut.ToString()); } catch (Exception ex) when (mode == Mode.NeverThrow) { return new ProcessRunnerResult(-1, sbStdErr.ToString(), sbStdOut.ToString(), ExceptionDispatchInfo.Capture(ex)); } catch (Exception ex) { throw new ProcessRunnerException(executablePath, args, -1, sbStdErr.ToString(), sbStdOut.ToString(), ex); } if (mode == Mode.RethrowAndIfNonZeroExitCode && process.ExitCode != 0) { throw new ProcessRunnerException(executablePath, args, process.ExitCode, sbStdErr.ToString(), sbStdOut.ToString()); } return new ProcessRunnerResult(process.ExitCode, sbStdErr.ToString(), sbStdOut.ToString()); } private static void Process_OutputDataReceived(string? data, Action<string>? notifyLine, StringBuilder sb) { if (data == null || data.Length == 0) { return; } sb.AppendLine(data); notifyLine?.Invoke(data); } }
补充信息
EDIT 1
虽然Action<Process>回调看似存在问题,但死锁发生时并未传入该类回调参数。传入回调的代码示例如下:
(process) => consoleOut.MarkupLine("[green]Command execution started. Press [[Esc]] to stop the command execution.[/]"), (process) => { if (!process.HasExited) { process.Kill(); } },
EDIT 2
已通过using关键字确保Process对象被正确释放,但两种实现版本仍周期性出现死锁,且死锁发生时未传入afterInvoke和onCancelInvoke回调。
内容的提问来源于stack exchange,提问作者mark
相关产品推荐
相关产品推荐

