使用Process類重定向時出現阻塞的解決方案

 

【摘要】
    使用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()方法才被調用,但此時它已沒有什麼用了,肯定立刻返回,向下執行。跟蹤調試了一下,驗證了自己的想法,於是乾脆去掉了這一句,經驗證並沒有對運行產生影響。

    以上很大部分都是自己的猜測,並不敢保證準確,也許很多地方理解有誤,歡迎各位大牛指正。

【全文結束】
發佈了43 篇原創文章 · 獲贊 6 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章