执行进程的 C# class 中的异步方法

Asynchronous method in a C# class that executes a process

我有一个 this post 的后续问题。在我的版本中,我有以下要异步的。这是我所拥有的:

    public virtual Task<bool> ExecuteAsync()
    {
        var tcs = new TaskCompletionSource<bool>();
        string exe = Spec.GetExecutablePath();
        string args = string.Format("--input1={0} --input2={1}", Input1, Input2);

        try
        {
            var process = new Process
            {
                EnableRaisingEvents = true,
                StartInfo =
                {
                    UseShellExecute = false,
                    FileName = exe,
                    Arguments = args,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    WorkingDir = CaseDir
                }
            };
            process.Exited += (sender, arguments) =>
            {
                if (process.ExitCode != 0)
                {
                    string errorMessage = process.StandardError.ReadToEndAsync();
                    tcs.SetResult(false);
                    tcs.SetException(new InvalidOperationException("The process did not exit correctly. Error message: " + errorMessage));
                }
                else
                {
                    File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd());
                    tcs.SetResult(true);
                }
                process.Dispose();
            };
            process.Start();
        }
        catch (Exception e)
        {
            Logger.InfoOutputWindow(e.Message);
            tcs.SetResult(false);
            return tcs.Task;
        }
        return tcs.Task;
    }
}

这里 Spec, Input1, Input2, CaseDir, LogFile 是 class 的所有成员,其中 ExecuteAsync 是一个方法。这样使用它们可以吗?我正在努力的部分是:

  1. 我似乎无法在方法定义 (public virtual async Task<bool> ExecuteAsync()) 中使用 async 关键字而没有警告我需要一个 await 关键字,而我在 lambda 表达式中确实有一个过程。我什至需要在方法定义中使用 async 关键字吗?我见过所谓的异步示例,但他们不使用它,例如this one。如果我把它拿出来编译,但是我可以异步使用它吗?
  2. 我在 lambda 表达式中使用 async 关键字以及在进程 lambda 表达式中相应的 await process.StandardError.ReadToEndAsync() OK 吗?在this example中,他们并没有在相应的行使用async await,所以我想知道他们是如何逃脱的?将它遗漏不会使其阻塞,因为我被告知方法 ReadToEnd 正在阻塞?
  3. 我对 File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd()) 的调用是否会导致整个方法阻塞?如果是这样,我怎样才能避免这种情况?
  4. 异常处理有意义吗?我应该知道我在 catch 块中使用的应用程序记录器方法 Logger.InfoOutputWindow 的任何细节吗?
  5. 最后,为什么在我遇到的所有示例中 process.Exited 事件总是出现在 process.Start() 之前?我可以将 process.Start() 放在 process.Exited 事件之前吗?

感谢任何想法,并提前感谢您的关注和关注。

编辑#1:

对于上面的#3,我有一个想法,部分基于下面@René Vogt 的评论,所以我做了一个更改,将 File.WriteAllText(...) 调用移动到 else {} 块内process.Exited 事件。也许这解决了#3.

编辑#2:

我制作了最初的更改列表(现在更改了代码片段),基本上删除了函数定义中的 async 关键字和 process.Exited 事件处理程序中的 await 关键字基于@René Vogt 的原始评论。还没有尝试过他最近的变化。当我 运行 时,我得到一个异常:

A plugin has triggered error: System.InvalidOperationException; An attempt was made to transition a task to a final state when it had already completed.

应用程序日志调用堆栈如下:

UNHANDLED EXCEPTION:
Exception Type:     CLR Exception (v4)
Exception Details:  No message (.net exception object not captured)
Exception Handler:  Unhandled exception filter
Exception Thread:   Unnamed thread (id 29560)
Report Number:      0
Report ID:          {d80f5824-ab11-4626-930a-7bb57ab22a87}
Native stack:
   KERNELBASE.dll+0x1A06D  RaiseException+0x3D
   clr.dll+0x155294
   clr.dll+0x15508E
   <unknown/managed> (0x000007FE99B92E24)
   <unknown/managed> (0x000000001AC86B00)
Managed stack:
   at System.Threading.Tasks.TaskCompletionSource`1.SetException(Exception exception)
   at <namespace>.<MyClass>.<>c__DisplayClass3.<ExecuteAsync>b__2(Object sender, EventArgs arguments)
   at System.Diagnostics.Process.RaiseOnExited()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading._ThreadPoolWaitOrTimerCallback.PerformWaitOrTimerCallback(Object state, Boolean timedOut)
  1. 您的方法签名不需要 async,因为您不使用 await。 return一个Task就够了。调用者可能await那个Task——或者不是,这与你的方法无关。

  2. 不要在该 lambda 上使用 async 关键字,也不要在该 lambda 中使用异步 ReadToEnd。很难预测如果你在事件处理程序真正完成之前 return 会发生什么。不管怎样,你想完成那个方法。进程退出的时候调用,不需要这样async.

  3. 这里同(2)。我认为在这个事件处理程序中执行 "synchronously" 是可以的。它只会阻塞这个处理程序,但是处理程序是在进程退出后调用的,所以我想你没问题。

  4. 您的异常处理看起来不错,但我会在 Exited 事件处理程序中添加另一个 try/catch 块。但这不是基于知识,而是基于任何地方都可能出错的经验:)


为了更好地获取标准和错误输出,我建议订阅 ErrorDataReceivedOutputDataReceived 事件并用接收到的数据填充 StringBuilders。

在你的方法中,声明两个 StringBuilders:

StringBuilder outputBuilder = new StringBuilder();
StringBuilder errorBuilder = new StringBuilder();

并在实例化后立即订阅事件 process:

process.OutputDataReceived += (sender, e) => outputBuilder.AppendLine(e.Data);
process.ErrorDataReceived += (sender, e) => errorBuilder.AppendLine(e.Data);

那你只需要在调用process.Start()之后调用这两个方法即可(之前是不行的,因为stdout和stderr还没有打开):

process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();

在您的 Exited 事件处理程序中,您可以调用 outputBuilder.ToString()(或分别为 errorBuilder.ToString())而不是 ReadToEnd,并且一切正常。

不幸的是,也有一个缺点:如果过程非常非常快,理论上您的 Exited 处理程序可能会在那些 Begin*ReadLine 调用之前被调用。不知道如何处理,但这不太可能发生。