C# Process.StandardInput未关闭StreamWriter写入时死锁挂起问题
C#与常驻C++进程标准IO双向交互死锁问题
问题背景
对接C++实现的常驻状态机进程时遇到标准输入输出挂起/死锁问题:
- C++进程标准输入逻辑完全符合规范,常驻运行,可通过标准输入接收指令、存储变量、解析表达式并返回计算结果,要求单次输出后仍可持续接收后续输入,不能关闭输入流、不能重启进程
- 公开资料给出的通用解决方案是写入完成后调用
Process.StandardInput.Close(),该方案确实能解决挂起问题,但关闭StandardInput对应的StreamWriter后后续交互无法进行,完全不适用于本场景 - 测试用C++进程逻辑为循环读取标准输入行并原样回显,代码如下:
int main () { std::string input; while (getline(std::cin, input)) { std::cout << input << std::endl; } }
初始实现问题
最初的C#封装类逻辑为启动重定向标准输入输出的进程,开启StandardInput.AutoFlush,Run方法写入输入后直接调用ReadToEnd读取输出。该代码仅当取消p.StandardInput.Close()注释时可正常运行——原因是ReadToEnd会持续读取直到流末尾,只有关闭输入流触发C++进程退出、输出流关闭时才会返回,关闭流后后续Run调用会直接失效。
初始实现代码:
public class Expression { System.Diagnostics.Process p; public Expression () { p = new System.Diagnostics.Process(); p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardInput = true; p.StartInfo.FileName = "InputEcho.exe"; p.Start(); p.StandardInput.AutoFlush = true; } public void Run (in string input, out string output) { p.StandardInput.WriteLine(input); // p.StandardInput.Close(); output = p.StandardOutput.ReadToEnd(); } }
故障现象:主程序连续两次传入字符串测试,预期依次打印
Hello、Hi,实际仅打印启动提示后程序就挂起无响应。
方案选型踩坑
后续尝试过基于事件委托的异步输出接收方案,但该方案难以适配同步收发的API封装需求:现有业务调用链路需要Run方法传入输入后直接返回对应输出,供上层逻辑同步判断结果,若采用全局容器存储异步接收的输出则存在竞态条件风险。
落地实现方案
最终实现了异步输出监听+输出队列+超时等待的封装方案:通过OutputDataReceived事件将接收的输出存入队列,Run方法仅负责写入输入,GetOutput方法阻塞等待队列中有数据时出队返回,同时加入超时机制避免无限等待。
代码实现如下:
public class Expression { System.Diagnostics.Process p = new(); System.Collections.Generic.Queue<string> outputQ = new(); public Expression () { p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardInput = true; p.StartInfo.FileName = "C2E2.exe"; p.OutputDataReceived += (s, e) => { outputQ.Enqueue(e.Data); }; p.Start(); p.BeginOutputReadLine(); } /// 遇到错误返回自定义异常对象 public GRLib.Exception Run (in string input) { if (p == null) return GRLib.Exception.New("Expression Evaluator not operational."); try { p.StandardInput.WriteLine(input); } catch (Exception e) { return GRLib.Exception.New(e.Message); } return null; } /// 超时返回错误码1,timeout单位为毫秒,传0则无限等待,阻塞调用 public GRLib.Exception GetOutput (out string output, int timeout = 2000) { int timeWaited = 0; while (outputQ.Count == 0) { System.Threading.Thread.Sleep(100); if (timeout != 0 && (timeWaited += 100) > timeout) { output = "ERR"; return GRLib.Exception.New(1, "Get timed out."); } } output = outputQ.Dequeue(); return null; } // 其余类实现省略 }
调用示例
Expression expression = new(); var e = expression.Run("3 > 2"); if (e != null) // 处理错误 string output; e = expression.GetOutput(out output); if (e != null) // 处理错误 // output即为返回结果"true",可直接供业务逻辑使用
待优化点
目前该方案可满足调用栈中输入输出同序列返回的需求,仍有两点待优化:
- 当前等待循环采用
Thread.Sleep(100)轮询实现,是否存在更优的等待循环实现方式 - 是否存在更优雅的常驻进程同步交互实现方案
内容的提问来源于stack exchange,提问作者GhostRavenstorm
相关产品推荐
相关产品推荐

