首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >不使用C# Process.StandardInput.Write死锁/挂起

不使用C# Process.StandardInput.Write死锁/挂起
EN

Stack Overflow用户
提问于 2022-07-13 02:25:42
回答 1查看 160关注 0票数 1

我有一个C#程序,它希望与用C++编写的外部进程进行交互。我相信这个C++过程使用的是正确的标准输入。我只是无法让我的C#代码在试图写到Process.StandardInput时不挂起。

在编写文章时,我已经看到了无数使用Process.StandardInput.Close()的例子。我找到的每一个StackOverflow答案都说要使用这个,它确实有效。问题是我不能关闭StreamWriter,因为我还没有完成与进程的交互。该进程是一个状态机,它保存使用stdin创建的变量,解析表达式,并返回一个计算。在每次输出之后,我都要继续给出流程输入。

有没有人有这样一个示例,在不关闭或重新启动进程的情况下使用Process.StandardInput.WriteLine不止一次?

这就是C++进程读取输入的方式。此示例简单地回显输入并等待另一个输入。

代码语言:javascript
运行
复制
int main () {
   std::string input;
   while (getline(std::cin, input)) {
      std::cout << input << std::endl;
   }
}

我的C#程序试图使用这个包装类与这个过程交互。

代码语言:javascript
运行
复制
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();
   }
}

当我取消注释p.StandardInput.Close()时,这是可行的,但随后对Expression.Run()的调用将无法工作,因为编写器已经关闭。

主程序

代码语言:javascript
运行
复制
Expression expn = new();
string output;

Console.WriteLine("Expression start");

expn.Run("Hello", output);
Console.WriteLine(output);

expn.Run("Hi", output);
Console.WriteLine(output);

预期产出

代码语言:javascript
运行
复制
Expression start
Hello
Hi

实际输出

代码语言:javascript
运行
复制
Expression start

编辑:

@Matthew给出了一个很好的答案,但这并不是我想要的结果。我没有想过使用事件委托来接收输出数据,我明白为什么:很难将其实现到我想要用来构建与流程相关的API的包装器中。我的意思是,我想编写一些方法来与进程通信,给它输入,接收输出,并在执行其他操作之前将这些数据返回给调用者。我的Expression.Run方法完美地证明了这一点。

下面是根调用者在更大的C#程序中的样子的一个例子。

代码语言:javascript
运行
复制
bool GetConditionEval (string condition, SomeDataType data) {

   // Makes another call to 'Run' that commands the C++ process to store a variable
   // Input looks like this: "variableName = true" (aka key/value pairs)
   Expression.SetVar(data.name, "true");

   // Don't ask why I'm using an external process to set variables using string expressions.
   // It's a company proprietary thing.

   string output;
   Expression.Run(in condition, out output);

   if (output.ToLower() == "true") return true;
   else if (output.ToLower() == "false") return false;
   else throw new Exception("Output is something other than true or false.");
}

这就是为什么我希望Run立即返回它从进程接收到的输出。如果没有,我想我可以为委托方法找到一种方法,将输出存储在全局容器中,而GetConditionEval可以直接进入这个范围。不过,我担心比赛的情况。

附带注意:由于我确实希望包含在这个C++过程中的API最终会以其他形式出现,因此将其作为一个独立的进程来处理,并且通过stdin调用API实际上是暂时的,因此我不必将数千行C++代码转换为C#。

解决办法:

我想出了一个解决方案,使用Matthew建议的异步方法,同时具有一个线性过程,即发送输入,并以相同的顺序立即关闭输出。我重新配置了包装类,以对从事件侦听器接收的每个输出进行排队。这就建立了一个模式,在这个模式中,我可以调用一个方法来发送输入,然后立即调用另一个方法将输出数据弹出队列(如果有的话)。我弥补了这样一个事实,即如果队列是空的,那么如果队列是空的,那么输出数据就不可能立即被保存下来,然后在有什么东西存在时继续前进。如果它需要等待的话,这会使它成为一个阻塞的呼叫,但这是迄今为止我所拥有的最好的。我还实现了一个故障安全,这样它就不会不确定地等待。

代码语言:javascript
运行
复制
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();
   }

   /// Returns custom exception object if error is encountered.
   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;
   }

   /// Returns error code 1 if timeout occured.
   /// Timeout is represented in milliseconds.
   /// Blocking call.
   public GRLib.Exception GetOutput (out string output, int timeout = 2000) {

      /// Wait for something to show in the queue.
      /// Waits indefinitely if timeout is 0.

      /// If anyone knows a better way to implement this waiting loop,
      /// please let me know!

      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;
   }

...
}

示例用法

代码语言:javascript
运行
复制
Expression expression = new();

var e = expression.Run("3 > 2");
if (e != null) // Handle error

string output;
e = expression.GetOutput(out output);
if (e != null) // Handle error

// 'output' should now be 'true' which can then be used in other parts of this program.

虽然事件侦听器以独立的方式工作很好,但我需要在输入所在的同一个堆栈中返回来自流程的输出,因为这将是一个更复杂的调用图的一部分。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-07-13 05:44:09

您所观察到的问题是由于Process.StandardOutput.ReadToEnd()的同步性质。相反,您应该通过设置Process.BeginOutputReadLine()和利用Process.OutputDataReceived事件异步地侦听输出。

下面是一个让您入门的快速示例:

代码语言:javascript
运行
复制
var p = new Process();
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = @"ConsoleApplication1.exe";

p.OutputDataReceived += (s, e) =>
{
    Console.WriteLine(e.Data);
};

p.Start();
p.BeginOutputReadLine();

while (true)
{
    var readLine = Console.ReadLine();
    p.StandardInput.WriteLine(readLine);
}

这是我用来做c++的ConsoleApplication1.exe

代码语言:javascript
运行
复制
int main()
{
    std::cout << "Hello World!\n";
    std::string input;
    while (std::getline(std::cin, input)) {
        std::cout << input << std::endl;
    }
}

运行我的示例将打印Hello World!,然后继续执行将您输入到控制台中的任何其他内容。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/72960241

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档