执行进程的 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 是一个方法。这样使用它们可以吗?我正在努力的部分是:
- 我似乎无法在方法定义 (
public virtual async Task<bool> ExecuteAsync()
) 中使用 async 关键字而没有警告我需要一个 await
关键字,而我在 lambda 表达式中确实有一个过程。我什至需要在方法定义中使用 async 关键字吗?我见过所谓的异步示例,但他们不使用它,例如this one。如果我把它拿出来编译,但是我可以异步使用它吗?
- 我在 lambda 表达式中使用 async 关键字以及在进程 lambda 表达式中相应的
await process.StandardError.ReadToEndAsync()
OK 吗?在this example中,他们并没有在相应的行使用async await
,所以我想知道他们是如何逃脱的?将它遗漏不会使其阻塞,因为我被告知方法 ReadToEnd
正在阻塞?
- 我对
File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd())
的调用是否会导致整个方法阻塞?如果是这样,我怎样才能避免这种情况?
- 异常处理有意义吗?我应该知道我在
catch
块中使用的应用程序记录器方法 Logger.InfoOutputWindow
的任何细节吗?
- 最后,为什么在我遇到的所有示例中
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)
您的方法签名不需要 async
,因为您不使用 await
。 return一个Task
就够了。调用者可能await
那个Task
——或者不是,这与你的方法无关。
不要在该 lambda 上使用 async
关键字,也不要在该 lambda 中使用异步 ReadToEnd
。很难预测如果你在事件处理程序真正完成之前 return 会发生什么。不管怎样,你想完成那个方法。进程退出的时候调用,不需要这样async
.
这里同(2)。我认为在这个事件处理程序中执行 "synchronously" 是可以的。它只会阻塞这个处理程序,但是处理程序是在进程退出后调用的,所以我想你没问题。
您的异常处理看起来不错,但我会在 Exited
事件处理程序中添加另一个 try/catch
块。但这不是基于知识,而是基于任何地方都可能出错的经验:)
为了更好地获取标准和错误输出,我建议订阅 ErrorDataReceived
和 OutputDataReceived
事件并用接收到的数据填充 StringBuilder
s。
在你的方法中,声明两个 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
调用之前被调用。不知道如何处理,但这不太可能发生。
我有一个 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 是一个方法。这样使用它们可以吗?我正在努力的部分是:
- 我似乎无法在方法定义 (
public virtual async Task<bool> ExecuteAsync()
) 中使用 async 关键字而没有警告我需要一个await
关键字,而我在 lambda 表达式中确实有一个过程。我什至需要在方法定义中使用 async 关键字吗?我见过所谓的异步示例,但他们不使用它,例如this one。如果我把它拿出来编译,但是我可以异步使用它吗? - 我在 lambda 表达式中使用 async 关键字以及在进程 lambda 表达式中相应的
await process.StandardError.ReadToEndAsync()
OK 吗?在this example中,他们并没有在相应的行使用async await
,所以我想知道他们是如何逃脱的?将它遗漏不会使其阻塞,因为我被告知方法ReadToEnd
正在阻塞? - 我对
File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd())
的调用是否会导致整个方法阻塞?如果是这样,我怎样才能避免这种情况? - 异常处理有意义吗?我应该知道我在
catch
块中使用的应用程序记录器方法Logger.InfoOutputWindow
的任何细节吗? - 最后,为什么在我遇到的所有示例中
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)
您的方法签名不需要
async
,因为您不使用await
。 return一个Task
就够了。调用者可能await
那个Task
——或者不是,这与你的方法无关。不要在该 lambda 上使用
async
关键字,也不要在该 lambda 中使用异步ReadToEnd
。很难预测如果你在事件处理程序真正完成之前 return 会发生什么。不管怎样,你想完成那个方法。进程退出的时候调用,不需要这样async
.这里同(2)。我认为在这个事件处理程序中执行 "synchronously" 是可以的。它只会阻塞这个处理程序,但是处理程序是在进程退出后调用的,所以我想你没问题。
您的异常处理看起来不错,但我会在
Exited
事件处理程序中添加另一个try/catch
块。但这不是基于知识,而是基于任何地方都可能出错的经验:)
为了更好地获取标准和错误输出,我建议订阅 ErrorDataReceived
和 OutputDataReceived
事件并用接收到的数据填充 StringBuilder
s。
在你的方法中,声明两个 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
调用之前被调用。不知道如何处理,但这不太可能发生。