使用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()方法才被調用,但此時它已沒有什麼用了,肯定立刻返回,向下執行。跟蹤調試了一下,驗證了自己的想法,於是乾脆去掉了這一句,經驗證並沒有對運行產生影響。
以上很大部分都是自己的猜測,並不敢保證準確,也許很多地方理解有誤,歡迎各位大牛指正。