使用Process类重定向时遇到死锁问题,对Process的实现机制进行了一番思考,想看全文就点进去吧。
【全文】
[系统环境]
.Net Framework 1.1,使用C#开发WinForm程序
[问题描述]
程序中要调用外部程序cmd.exe执行一些命令行,并取得屏幕输出,使用了Process类,基本代码如下:
Process process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
………… //一些处理
process.StandardInput.WriteLine("exit");
//取输出内容
String outputString = process.StandardOutput.ReadToEnd();
process.WaitForExit();
process.Close();
实际使用中发现有时程序会无响应,并且是在执行某些特定的命令时才会无响应。跟踪调试发现,问题出在process.StandardOutput.ReadToEnd()上,在这一行按F10,程序不向下执行,也不产生Exception。
[解决过程]
由于调试时程序不向下执行,猜测ReadToEnd()没有返回,被阻塞住了,查了查MSDN,没有看到有相关内容。又反编了一下mscorlib.dll,看了看StreamReader.ReadToEnd()的实现方法,是这么一段代码:
public override string ReadToEnd()
{
int num1;
if (this.stream == null)
{
__Error.ReaderClosed();
}
char[] chArray1 = new char[this.charBuffer.Length];
StringBuilder builder1 = new StringBuilder(this.charBuffer.Length);
while ((num1 = this.Read(chArray1, 0, chArray1.Length)) != 0)
{
builder1.Append(chArray1, 0, num1);
}
return builder1.ToString();
}
从中看出ReadToEnd()是利用Read()方法实现的,又去MSDN看了,仍然没有提到Read()是阻塞函数,于是只能上网去查一查相关资料,有人提到了类似的问题,并且给出了解决方法:使用两个线程分别做StandardOutput.ReadToEnd()和StandardError.ReadToEnd()。试了一下,果然可以解决。
[后续思考]
问题虽然解决,不过原理没有弄明白,还要继续深入。查阅相关资料,大致了解了Process的运作模式,它以子进程的形式启动cmd.exe,如果指定了重定向Input、Output或Error,就建立相应的管道在父子进程之间进行通讯。
基于这些内容以及关于管道的理解,得到以下几条猜测:
1、input、output、error的三条管道是相互独立的。
2、管道有大小,空时不可读,满时不可写。
3、遇到管道不可读写时,相应的进程会阻塞等待可读写为止。
4、子进程结束前,output流与error流不会结束。
这样再翻回来考虑之前遇到的问题,可以猜测无响应时是这样一种情况:
建立Process时指定了Output和Error都重定向,父子进程间就建立了2条管道,cmd.exe不停输出output与error,分别进入两条管道,对output,管道满后,子进程会停下来等待父进程取走数据,父进程的StandardOutput.ReadToEnd()方法正是取数据的,它在管道空时会处于阻塞状态,有数据时就取走,这样子进程会继续写output。
但是对于error,父进程没有相应的ReadToEnd()方法,很快error的管道就满了,由于无法写error,子进程会停下来等待,但是父进程永远不读,子进程也就永远不再继续,形成死锁。
对于不出问题的情况,一定是error管道根本就没有写满,这也就解释了为什么命令的内容决定了是否会出现无响应的情况。
那么直接在StandardOutput.ReadToEnd()后面加一条StandardError.ReadToEnd()行不行呢?答案是不行的,基于上面的猜测与分析,ReadToEnd()方法是阻塞的,在子进程结束前,output流不会关闭,所以StandardOutput.ReadToEnd()会一直等待,造成后面的StandardError.ReadToEnd()方法根本不被执行,还是会产生死锁。所以解决方案里才提到了要建立2个线程,让两个管道的ReadToEnd()方法独立执行。
再回来考虑我的程序中的情况,之所以没有去读Error,是因为我根本不关心子进程产生的error输出,所以应该有更简单的方法,就是把process.StartInfo.RedirectStandardError置为false,经测试验证,想法是对的, 这样还避免了使用线程。
在分析的过程中还对process.WaitForExit()方法产生了兴趣,从msdn的描述来看,它的作用是等待子进程结束,应该也是阻塞的,但在我的程序中它在ReadToEnd()方法后面,是否还有实际作用呢?猜测子进程结束后,ReadToEnd()方法才返回,WaitForExit()方法才被调用,但此时它已没有什么用了,肯定立刻返回,向下执行。跟踪调试了一下,验证了自己的想法,于是干脆去掉了这一句,经验证并没有对运行产生影响。
以上很大部分都是自己的猜测,并不敢保证准确,也许很多地方理解有误,欢迎各位大牛指正。


如Oracle的exp.exe
是否具有普遍性尚未考证。
Use asynchronous read operations. See MSDN.
Follow these steps to perform asynchronous read operations on StandardOutput for a Process :
1. Set UseShellExecute to false.
2. Set RedirectStandardOutput to true.
3. Add your event handler to the OutputDataReceived event. The event handler must match the System.Diagnostics..::.DataReceivedEventHandler delegate signature.
4. Start the Process.
Call BeginOutputReadLine for the Process. This call starts asynchronous read operations on StandardOutput.
When asynchronous read operations start, the event handler is called each time the associated Process writes a line of text to its StandardOutput stream.