【问题标题】:How to synchronize writing and async reading from interactive process's std's如何从交互式进程的 std 同步写入和异步读取
【发布时间】:2016-11-22 10:41:40
【问题描述】:

我正在尝试构建一个使用 python 动态解释器及其 eval 函数的 wpf 应用程序。 编辑:我已经给出了更详细的描述here 简单来说,我希望能够执行以下操作:

string expression = Console.ReadLine("Please enter your expression");
if (EvaluateWithPythonProcess(expression) > 4)
{
  // Do something
}
else
{
  // Do something else
}

由于我的程序在其整个生命周期中都使用此功能,因此每次我想开始评估时都无法退出 python 进程。因此,StdIn、StdOut 和 StdErr 流始终保持打开状态。

我能够使用 Process 类和两个相应的 OnOutputDataReceived 和 OnErrorDataReceived 方法启动交互式 python.exe,它们将数据从 stdOut 和 stdErr 传输到 StringBuilders:

// create the python process StartupInfo object
                ProcessStartInfo _processStartInfo = new ProcessStartInfo(PythonHelper.PathToPython + "python.exe");

                // python uses "-i" to run in interactive mode
                _processStartInfo.Arguments = "-i";

                // Only start the python process, but don't show a (console) window
                _processStartInfo.WindowStyle = ProcessWindowStyle.Minimized;
                _processStartInfo.CreateNoWindow = true;

                // Enable the redirection of python process std's
                _processStartInfo.UseShellExecute = false;

                _processStartInfo.RedirectStandardOutput = true;
                _processStartInfo.RedirectStandardInput = true;
                _processStartInfo.RedirectStandardError = true;

                // Create the python process object and apply the startupInfos from above
                _pythonProcess = new Process();
                _pythonProcess.StartInfo = _processStartInfo;

                // Start the process, _hasStarted indicates if the process was actually started (true) or if it was reused (false, was already running)

                    _pythonProcess.OutputDataReceived += new DataReceivedEventHandler(OnOutputDataReceived);
                    _pythonProcess.ErrorDataReceived += new DataReceivedEventHandler(OnErrorDataReceived);
                    bool _hasStarted = _pythonProcess.Start();

                    _pythonProcess.BeginOutputReadLine();
                    _pythonProcess.BeginErrorReadLine();
                    _input = _pythonProcess.StandardInput;

但是,我无法将我的应用程序与这种异步结果收集同步。由于两个 On*DataReceived() 方法是异步调用的,我不知道 python 是否完成了对我的表达式的评估。一个可能的解决方案是在向 pythons stdIn 发送命令之前创建一个等待句柄,之后我可以等待。 OnOutputDataReceived 和 OnErrorDataReceived 方法都可以用信号通知这个句柄。然而,这在某种程度上被 python 的预期行为所掩盖:

                // example A: Import the sys modul in python 
                // this does cause neither an output, nor an error:
                _input.WriteLine("import sys");

                // example B: Writing to pythons stderr or stdout results in Error AND Output, how can I tell if an error occured?
                _input.WriteLine("sys.stderr.write('Initialized stdErr')");

                _input.WriteLine("sys.stdout.write('Initialized stdOut')");

                // example C: This is the intended use, but how can I tell if evaluation has finished succesfully?
                _input.WriteLine("print(4+7)");

                // example D: A simple typo might lead to unforeseeable errors but how can I tell if evaluation has finished succesfully?
                _input.WriteLine("pr int(4+7)");

【问题讨论】:

  • 这个问题太笼统了。你有很多不同的场景你要问,但你没有提供确切的说明你想如何解决它们,也没有说明到目前为止你已经尝试过什么。请提供一个好的minimal reproducible example,清楚地显示您尝试过的内容,以及对代码的作用以及您希望它做什么的精确描述。同时,请记住 stdin 也被缓冲,所以如果您不期望特定的输出,您可以发送下一个输入。
  • 至于跟踪什么是错误,什么不是,您必须做与人类相同的事情:根据您在 stdout 和/或 stderr 上收到的消息进行推断。实际上对于每个流的内容没有任何强制规则,一些进程使用 stdout 处理错误,而其他进程使用 stderr 输出或信息文本(例如警告)。由您决定如何处理任何一个流的输出。
  • 嗯,我明白了基于上下文评估结果的重点。但是我的问题在我能做到之前就出现了:我怎么知道子进程已经收到我的命令,并完成了相应的操作(参见上面的示例 A:我无法判断 python 是否理解这个导入,或者卡在一个例如死锁...)
  • 为了评估我已经做过的事情,请参阅我昨天尝试同步方法的帖子:stackoverflow.com/questions/38430330/…
  • “我怎么知道子进程已经收到了我的命令,并且已经完成了相应的动作”——和人类一样:你读输出。如果没有输出,您将遇到与人类相同的问题:您不知道进程是否被卡住,或者它是否已准备好执行新命令。您所能做的就是尝试输入一个新命令,看看是否有任何输出。

标签: c# multithreading asynchronous stdout stdin


【解决方案1】:

我找到了一个解决方案,我认为它是针对我的特定场景的解决方法,而不是通用解决方案。

根据@Peter 的 cmets,我试图弄清楚“人类将如何解决这个问题”

  1. 我必须确保子进程仅通过 stdOut 与父进程通信。
  2. 我必须创建一个基于消息的协议,以确保子 python 进程始终报告他是否已收到并理解父 c# 进程发送的消息,如果是,则报告表达式的评估值。
  3. 我得想办法让后续的读写同步

第 1 点和第 2 点是通过定义一个 python 方法来实现的,该方法将始终是我的父进程的目标。我利用pythons异常处理例程来检测错误并防止它写入stdErr,如下所示:

def PythonEval(expression):
    "Takes an expression and uses the eval function, wrapped in a try-except-statement, to inform the parent process about the value of this expression"
    try:
      print(eval(expression))
      print('CHILD: DONE')
    except:
      print('CHILD: ERROR')
    return

可以在我的 c# 应用程序中应用此定义,方法是将 python 代码包装在一个字符串中并将其传递给子进程的 stdIn:

childInput = childProcess.StandardInput;

childInput.WriteLine("def PythonEval(expression):\n" +  
    "\t\"Takes an expression and uses the eval function, wrapped in a try-except-clause, to inform the LMT about the outcome of this expression\"\n" + 
    "\ttry:\n" +
        "\t\tprint(eval(expression))\n" + 
        "\t\tprint('" + _pythonSuccessMessage + "')\n" + 
    "\texcept:\n" + 
        "\t\tprint('" + _pythonErrorMessage + "')\n" + 
    "\treturn\n" + 
    "\n"); // This last newline character is important, it ends the definition of a method if python is in interactive mode 

如果我想在子进程的帮助下评估一个表达式,父进程现在总是必须将表达式包装在这个 python 方法的相应调用中:

childInput.WriteLine("PythonEval('" + expression + "')");

在所有情况下,这都会向子进程的 stdOut 发送一条消息,该消息的最后一行格式为“CHILD: DONE|ERROR”,我可以比较它并在后一种情况下设置一个布尔标志 _hasError。整个消息被传递到 stringBuilder OutputMessage。

当子进程将此消息发送到其 stdOut 时,会触发 C# 进程对象的 OutputDataReceivedEvent 并通过 OnOutputDataReceived 方法异步读取数据。为了与进程的异步读取操作同步,我使用了 AutoResetEvent。它允许将父 c# 进程与 python 进程同步,并通过使用 AutoResetEvent.WaitOne(int timeout) 重载来防止死锁。

AutoResetEvent 在将命令发送到 python 的特定方法中手动重置,在 waitOne 完成后(发生超时之前)自动重置,并在异步 OnOutputDataReceived() 方法中手动设置,如下所示:

private AutoResetEvent _outputResetEvent;
private bool _hasError;
private StringBuilder _outputMessage;

private void EvaluateWithPython(string expression)
{
    // Set _outputResetEvent to unsignalled state
    _outputResetEvent.Reset();

    // Reset _hasError, 
    _hasError = true;

    // Write command to python, using its dedicated method
    childInput.WriteLine("PythonEval('" + expression + "')"); // The ' chars again are important, as the eval method in python takes a string, which is indicated by 's in python

    // wait for python to write into stdOut, which is captured by OnOutputDataReceived (below) and sets _outputResetEvent to signalled stat
    bool _timeoutOccured = _outputResetEvent.WaitOne(5000);

    // Give OnOutputDataReceived time to finish
    Task.Delay(200).Wait();        
}

private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
{
    if (e == null)
    {
        throw new ArgumentNullException();
    }

    if (e.Data != null)
    {
        // Pass message into StringBuilder line by line, as OnOutputDataReceived is called line by line
        _outputMessage.AppendLine(e.Data);

        // Check for end of message, this is in all cases of the form "CHILD: DONE|ERROR"
        // In this case, set errorFlag if needed and signal the AutoResetEvent
        if (e.Data.Equals("CHILD: ERROR"))
        {
            _hasError = true;
            _outputResetEvent.Set();
        }
        else if (e.Data.Equals("CHILD: DONE"))
        {
            _hasError = false;
            _outputResetEvent.Set();
        }
    }
    else
    {
        // TODO: We only reach this point if child python process ends and stdout is closed (?)
        _outputResetEvent.Set();
    }


}

通过这种方法,我可以调用 EvaluateWithPython 并且可以同步:

  • 检查 python 是否在超时发生之前完成(如果没有,则以某种方式做出反应)
  • 如果没有发生超时,我知道 _hasError 告诉评估是否成功
  • 如果是这种情况,outputMessage 会在倒数第二行中包含结果。

为了应对任何监督的问题,我还将编写一个 OnErrorDataReceived 方法,该方法将捕获 Python 进程中未处理的异常和语法错误,并且可以抛出异常,因为这在我看来永远不会发生。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-27
    • 1970-01-01
    相关资源
    最近更新 更多