为什么ProcessStartInfo在“WaitForExit”上挂起?
为什么ProcessStartInfo在“WaitForExit”上挂起?
我有以下代码:
info = new System.Diagnostics.ProcessStartInfo("TheProgram.exe", String.Join(" ", args)); info.CreateNoWindow = true; info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; info.RedirectStandardOutput = true; info.UseShellExecute = false; System.Diagnostics.Process p = System.Diagnostics.Process.Start(info); p.WaitForExit(); Console.WriteLine(p.StandardOutput.ReadToEnd()); //需要StandardOutput的内容
我知道我启动的进程的输出大约为7MB。在Windows控制台中运行正常。不幸的是,在程序中,这个代码在WaitForExit
处无限期地挂起。请注意,对于较小的输出(如3KB),此代码不会挂起。
是否可能是ProcessStartInfo
中的内部StandardOutput
无法缓冲7MB?如果是这样,我应该怎么做?如果不是,我做错了什么?
ProcessStartInfo hanging on "WaitForExit"? Why?
在使用ProcessStartInfo的过程中,如果不按照文档中的建议,在等待之前先读取StandardOutput,就可能导致死锁。具体代码如下:
// Start the child process. Process p = new Process(); // Redirect the output stream of the child process. p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.FileName = "Write500Lines.exe"; p.Start(); // Do not wait for the child process to exit before // reading to the end of its redirected stream. // p.WaitForExit(); // Read the output stream first and then wait. string output = p.StandardOutput.ReadToEnd(); p.WaitForExit();
在某些环境下,如果设置了`RedirectStandardOutput = true;`,并且没有使用`p.StandardOutput.ReadToEnd();`,就会发生死锁。
即使已经重定向并读取了标准输出,这个问题仍然会发生。
我猜这只适用于StandardOutput的缓冲区没有完全填满的情况。这里MSDN的解释不够详细。我建议阅读一篇很好的文章:dzone.com/articles/async-io-and-threadpool。
解决方法暂时没有给出。
ProcessStartInfo中的WaitForExit问题的原因是,如果重定向了StandardOutput和/或StandardError,内部缓冲区可能会变满。无论使用什么顺序,都可能会出现问题:
- 如果在读取StandardOutput之前等待进程退出,进程可能会因为尝试写入StandardOutput而阻塞,从而进程永远不会结束。
- 如果使用ReadToEnd从StandardOutput中读取内容,如果进程永远不关闭StandardOutput(例如,如果它永远不终止或者在写入StandardError时被阻塞),那么你的进程可能会被阻塞。
解决方法是使用异步读取来确保缓冲区不会变满。为了避免任何死锁并收集来自StandardOutput和StandardError的所有输出,可以按照以下步骤进行操作:
using (Process process = new Process()) { process.StartInfo.FileName = filename; process.StartInfo.Arguments = arguments; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; StringBuilder output = new StringBuilder(); StringBuilder error = new StringBuilder(); using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) { process.OutputDataReceived += (sender, e) => { if (e.Data == null) { outputWaitHandle.Set(); } else { output.AppendLine(e.Data); } }; process.ErrorDataReceived += (sender, e) => { if (e.Data == null) { errorWaitHandle.Set(); } else { error.AppendLine(e.Data); } }; process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); if (process.WaitForExit(timeout) && outputWaitHandle.WaitOne(timeout) && errorWaitHandle.WaitOne(timeout)) { // Process completed. Check process.ExitCode here. } else { // Timed out. } } }
以上就是解决ProcessStartInfo中WaitForExit问题的方法,通过使用异步读取来避免缓冲区满的问题。这个方法已经得到了广泛的应用并解决了许多人的问题。
问题:为什么ProcessStartInfo在"WaitForExit"上挂起?解决方法是什么?
在使用ProcessStartInfo启动进程并等待其退出时,可能会出现挂起的问题。问题出现的原因是,在进程开始后但退出事件附加之前,进程可能已经结束。解决方法是在所有注册操作完成后再开始进程。
具体实现的代码如下:
public static async TaskStartProcess( string filename, string arguments, string workingDirectory = null, int? timeout = null, TextWriter outputTextWriter = null, TextWriter errorTextWriter = null) { using (var process = new Process() { StartInfo = new ProcessStartInfo() { CreateNoWindow = true, Arguments = arguments, FileName = filename, RedirectStandardOutput = outputTextWriter != null, RedirectStandardError = errorTextWriter != null, UseShellExecute = false, WorkingDirectory = workingDirectory } }) { var cancellationTokenSource = timeout.HasValue ? new CancellationTokenSource(timeout.Value) : new CancellationTokenSource(); process.Start(); var tasks = new List (3) { process.WaitForExitAsync(cancellationTokenSource.Token) }; if (outputTextWriter != null) { tasks.Add(ReadAsync( x => { process.OutputDataReceived += x; process.BeginOutputReadLine(); }, x => process.OutputDataReceived -= x, outputTextWriter, cancellationTokenSource.Token)); } if (errorTextWriter != null) { tasks.Add(ReadAsync( x => { process.ErrorDataReceived += x; process.BeginErrorReadLine(); }, x => process.ErrorDataReceived -= x, errorTextWriter, cancellationTokenSource.Token)); } await Task.WhenAll(tasks); return process.ExitCode; } } public static Task WaitForExitAsync( this Process process, CancellationToken cancellationToken = default(CancellationToken)) { process.EnableRaisingEvents = true; var taskCompletionSource = new TaskCompletionSource
以上是截至目前为止最好和最完整的解决方案。这是唯一有效的解决方案,能够防止应用程序挂起。原因是在进程启动后但退出事件附加之前,进程可能已经结束。解决方法是在所有注册操作完成后再开始进程。
另外,还需注意的是,在调用process.BeginOutputReadLine()或process.BeginErrorReadLine()之前,必须先调用process.Start()。否则会出现错误:"StandardOut has not been redirected or the process hasn't started yet"。
有用户还指出,在事件处理程序中移除事件处理程序本身是一种不太优雅的做法。希望能找到更好的解决方法。这也是唯一一个不会在读取输出流时导致挂起的方法。
更多细节请参考[此回答](https://stackoverflow.com/a/56582093/893335)。