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

子进程输出重定向时出现死锁问题排查(已遵循推荐模式)

子进程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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:04:53