由StreamWriter.WriteLine 引發對C#多線程的深入思考(一)


首先,StreamWriter線程安全麼?

答:StreamWriter 的構造以及StreamWriter.WriteLine(string)都是非線程安全的

我們封裝兩個寫日誌的方法。

底層都是由StreamWriter.writeline來實現.一個加鎖,一個不加鎖。將加鎖的那個命名爲safewritelog,另一個命名爲unsafeWritelog.然後利用兩個循環。不停的分別創建個線程,去寫日誌。測試看哪個會出現寫異常。代碼如下:


namespace ThreadWriteLog
{
    class Program
    {

        private static object ob = "喲內容!!";

        static void Main(string[] args)
        {


            for (int i = 0; i < 10; i++)
            {
                Thread wrtieThread = new Thread(SafyWriteLog);
                wrtieThread.Name = "線程--" + i;
                string content = "這是" + wrtieThread.Name + "的內容Y";
                wrtieThread.Start(content);
            }

            for (int i = 0; i < 10; i++)
            {
                Thread wrtieThread = new Thread(UnSafyWriteLog);
                wrtieThread.Name = "線程¨¬--" + i;
                string content = "這是" + wrtieThread.Name + "的內容Y";
                wrtieThread.Start(content);
            }
            Console.WriteLine("結束");
            Console.Read();
        }


      

        
        public  static void SafyWriteLog(object  content)
        {
            string path = @"C:\SafeLog.txt";
            lock (ob)
            {
                StreamWriter sw = File.AppendText(path);
                sw.WriteLine(content.ToString());
                sw.Close();
            }
        }

        public static void UnSafyWriteLog(object content)
        {
            string path = @"C:\UnSafeLog.txt";
            StreamWriter sw = File.AppendText(path);
            sw.WriteLine(content.ToString());
            sw.Close();
        }
    }
}


運行後,第一個for循環順利結束,文件中顯示 0-9進程沒有問題。

這是線程--0的內容

這是線程--1的內容

這是線程--2的內容

這是線程--5的內容

這是線程--3的內容

這是線程--4的內容

這是線程--6的內容

這是線程--7的內容

這是線程--8的內容

這是線程--9的內容

也符合線程的概念,隨着系統的隨機調度而運行。


而第二個for循環沒有正常完成,拋出異常

 

 

未處理的異常: 未處理的異常: 未處理的異常: 未處理的異常: 未處理的異常: 未處理的異
常:       System.IO.IOException: 文件“C:\UnSafeLog.txt”正由另一進程使用,因此
該進程無法訪問該文件。
   在 System.IO.__Error.WinIOError(Int32 errorCode, StringmaybeFullPath)
   在 System.IO.FileStream.Init(String path, FileMode mode,FileAccess access, I
nt32rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions o
ptions,SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)
   在 System.IO.FileStream..ctor(String path, FileMode mode,FileAccess access,
FileShareshare, Int32 bufferSize, FileOptions options)
   在 System.IO.StreamWriter.CreateFile(String path, Booleanappend)
   在 System.IO.StreamWriter..ctor(String path, Booleanappend, Encoding encodin
g, Int32bufferSize)
   在 System.IO.StreamWriter..ctor(String path, Booleanappend)
   在 System.IO.File.AppendText(Stringpath)
   在 caTestProj.Program.UnSafyWriteLog(Object content) 位置 F:\ASP.NET\MyCode\c
aTestProj\caTestProj\Program.cs:行號 51
   在 System.Threading.ThreadHelper.ThreadStart_Context(Objectstate)
   在 System.Threading.ExecutionContext.Run(ExecutionContextexecutionContext, C
ontextCallbackcallback, Object state)
   在 System.Threading.ThreadHelper.ThreadStart(Objectobj)System.IO.IOException
: 文件“C:\UnSafeLog.txt”正由另一進程使用,因此該進程無法訪問該文件。
   在 System.IO.__Error.WinIOError(Int32 errorCode, StringmaybeFullPath)


 

正常分析理解,

StreamWriter.WriteLine方法本身沒有線程同步方法,多線程寫日誌時(注意這裏,我們不同的線程使用的是不同的StreamWriter),多個線程同時訪問文件,出現異常。

但是 確實是WriteLine出錯了麼?

從堆棧跟蹤來看,錯誤出現在Thread線程回調UnSafyWriteLog方法出現錯誤,即執行AppendText時出錯。

在到裏邊看,構造StreamWriter對象出錯-à FileStream對象構造出錯-- 調用FileStream.Init出錯,最後到了win32函數winIoError.也就是構造FileStream對象時出錯。我們很明白肯定一個共享寫的問題了。

那麼,可以斷定,問題在於,調用File.APpendTest時,會構造StreamWriter,而這個StreamWriter是獨佔式的

由於該文件已被另一個線程訪問,所以StreamWriter構造出現異常,

而並不是在StreamWriter.WriteLine上出的錯誤。

對於SafeWritelog,我們對Streamwriter的構造以及write都加了鎖,也就是說,每次構造streamWriter的時候,還是Writeline的時候,我們都保證了有唯一的對象對磁盤文件(或者緩存)進行操作。


那如果我用同一個StreamWriter呢?也就是說,Writeline本身是不是多線程安全的?

現在我們使用同一個Streamwriter,測試writeline的線程安全特性。

也就是說,如果同時有兩個線程同時調用Wtriteline方法,出現異常,則說明非線程安全可沒有異常,說明線程安全。

代碼如下


    class Program
    {
        static object   ob=new object();
        static string path = @"C:\UnSafeLog.txt";
        private static StreamWriter sw2;

        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Thread wrtieThread = new Thread(SafyWriteLog);
                wrtieThread.Name = "線程--" + i;
                string content = "這是" + wrtieThread.Name + "的內容";
                wrtieThread.Start(content);
            }
           sw2 = File.AppendText(path);
            for (int i = 0; i < 10; i++)
            {
                Thread wrtieThread = new Thread(UnSafyWriteLog);
                wrtieThread.Name = "線程--" + i;
                string content = "這是" + wrtieThread.Name + "的內容Y";
                wrtieThread.Start(content);
            }
             sw2.Close();
            Console.WriteLine("結束");
            Console.Read();

        }


        public static void SafyWriteLog(object content)
        {
            string path = @"C:\SafeLog.txt";
            lock (ob)
            {
                StreamWriter sw = File.AppendText(path);
                sw.WriteLine(content.ToString());
                sw.Close();
            }
        }

        public static void UnSafyWriteLog(object content)
        {

            sw2.WriteLine(content.ToString());

        }

    }


運行後,貌似沒出現什麼問題。兩個for循環都正常執行完畢了。

而且日誌文件中的記錄也是按順序來的,沒有出現日誌文字錯亂現象。

這是線程--0的內容

這是線程--1的內容

這是線程--2的內容

這是線程--3的內容

這是線程--4的內容

這是線程--5的內容

這是線程--6的內容

這是線程--7的內容

這是線程--8的內容

這是線程--9的內容

但真的是這樣的麼?構造是非線程安全的,而writeline是線程安全的??不可能的。

仔細考慮,只有在某一個線程阻塞在WriteLine的時候,另一個線程也訪問該方法的時候,纔會出現多線程寫的情況。

那麼,ok,我們加大一次文字的寫入量,使其阻塞在WriteLine這裏

現在,我們構造更加簡單的場景,創建兩個線程,一次寫入大量日誌,使用同一個StreamWriter對象。如果writeline方法是非線程安全的,那麼肯定會出現異常。

代碼如下:


    class Program
    {
        public static StreamWriter sw = new StreamWriter("C:\\threadlog.txt", true);
        private static string path = @"C:\Users\cjt.IT\Desktop\20110807\S20110807031902.info";//大文件內容10M級
        private Thread t1;
        private Thread t2;

        static void Main(string[] args)
        {
            Program p=new Program();
            p.Test();
        }


        public void Test()
        {
            StreamReader sr = new StreamReader(path);
            string content = sr.ReadToEnd();
            BeginWrite1(content);
            BeginWrite2(content);
            sr.Close();
            //t1.Join();
            //sw.Close();
        }

        private void BeginWrite1(string content)
        {
            t1 = new Thread(WriteLog);
            t1.Start("---------線程1Begin--------" + Environment.NewLine + content + "---------線程1 End--------" + Environment.NewLine);
        }

        private void BeginWrite2(string content)
        {
            t2 = new Thread(WriteLog);
            t2.Start("---------線程2Begin--------" + Environment.NewLine + content + "---------線程2End--------" + Environment.NewLine);
        }

        private void WriteLog(object content)
        {
            sw.WriteLine(content.ToString());//內容過多線程會阻塞在該處
            sw.Flush();//這a樣能夠保證緩o衝區內的數據全部寫磁盤¨¬
        }

    }

運行之後,立即出現異常。


未處理的異常:  System.IndexOutOfRangeException: 在複製內存時檢測到可能的 I/O 爭
用條件。默認情況下,I/O 包不是線程安全的。在多線程應用程序中,必須以線程安全方式
(如 TextReader 或 TextWriter 的 Synchronized 方法返回的線程安全包裝)訪問流。這也
適用於 StreamWriter 和 StreamReader 這樣的類。
   在 System.Buffer.InternalBlockCopy(Array src, Int32 srcOffset, Array dst, Int
32 dstOffset, Int32 count)
   在 System.IO.StreamWriter.Write(Char[] buffer, Int32 index, Int32 count)
   在 System.IO.TextWriter.WriteLine(String value)
   在 caTestProj.Program.WriteLog(Object content) 位置 F:\ASP.NET\MyCode\caTestP
roj\caTestProj\Program.cs:行號 51
   在 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, C
ontextCallback callback, Object state)
   在 System.Threading.ThreadHelper.ThreadStart(Object obj)
請按任意鍵繼續. . .

從堆棧跟蹤來看:

在調用TextWriter.WriteLine(Streamwriter繼承於此)時出錯,系統要進行內存拷貝,出現I/O爭用。也就是說,線程不安全本質是由(至少該例子中是由I/o爭用導致的)。兩者都要將自己的內容拷貝到磁盤上,顯然要出錯。writeline方法缺少同步機制,拋出異常。

這裏是日誌內容一部分



可以看出來中間有個線程2begin,可以肯定,在這裏,線程1暫時阻塞,然後線程2開始寫,也就是說從這裏開始,日誌文件就開始混亂了,因爲1已經阻塞了,這裏沒有出現異常。

日誌文件結束



中間並沒有出現線程2end的標記,說明,線程2中間也阻塞了。這是啓動了線程1,造成中間這個界限丟失了。也有可能從線程2begin這一段,線程1和線程2就是混着寫的。直到他們兩個突然決定同時寫,系統出現I/O爭用錯誤,拋出異常。

 

如果我對其加鎖呢?

顯然我們如果控制了同時writelien的只有一個線程。那麼寫日誌就不會有問題。如果一個阻塞在writeline處,那麼另一個就會阻塞在lock外。等待阻塞線程釋放鎖後,進入該代碼段。那麼可以斷定,日誌文件也是有邏輯的。

代碼如下


class Program
    {
        public static StreamWriter sw = new StreamWriter("C:\\threadlog.txt", true);
        private static string path = @"C:\Users\cjt.IT\Desktop\S20110908002112.info";//大䨮文?件t內¨²容¨Y10M級?
        private Thread t1;
        private Thread t2;

        static void Main(string[] args)
        {
            Program p=new Program();
            p.Test();
        }


        public void Test()
        {
            StreamReader sr = new StreamReader(path);
            string content = sr.ReadToEnd();
            BeginWrite1(content);
            BeginWrite2(content);
            sr.Close();
            //t1.Join();
            //sw.Close();
        }

        private void BeginWrite1(string content)
        {
            t1 = new Thread(WriteLog);
            t1.Start("---------線?程¨¬1Begin--------" + Environment.NewLine + content + "---------線?程¨¬1 End--------" + Environment.NewLine);
        }

        private void BeginWrite2(string content)
        {
            t2 = new Thread(WriteLog);
            t2.Start("---------線?程¨¬2Begin--------" + Environment.NewLine + content + "---------線?程¨¬2End--------" + Environment.NewLine);
        }

        private void WriteLog(object content)
        {
            lock (path)//如果不加同步 由於 writeLine會發生阻塞.所´以當À另外一個?線?程¨¬也°2到Ì?WriteLine處ä|的Ì?時º¡À候¨°,ê?會¨¢發¤¡é生¦¨²  同ª?時º¡À寫¡ä 異°¨¬常¡ê
            {
            sw.WriteLine(content.ToString());//內¨²容¨Y過y多¨¤,ê?線?程¨¬會¨¢阻Á¨¨塞¨?在¨²該?處ä|
            sw.Flush();//這a樣¨´能¨¹夠?保À¡ê證¡è 緩o衝?區?內¨²的Ì?數ºy據Y全¨?部?寫¡ä入¨?磁ä?盤¨¬
            }
        }

    }

正常運行。查看日誌結果



可以看到可以認爲是兩個線程順序執行了。沒有出現混亂的情況。

(你可能已經注意到,上邊的代碼有兩個問題,1 sw沒有關閉 2 由於線程是前臺線程,main結束後,該線程並沒有結束,也就是說,我們不能單從窗口顯示”結束”,已到達main結尾來斷定寫日誌完成,我們需要等待一段時間,認爲寫日誌完成了,再去查看日誌)

但是,加鎖後,這樣的線程還有意義麼

線程的目的是使得程序併發進行。也就是說,如果我們要想使得寫日誌加快,可以採用多線程寫日誌。但是,按照上邊的情況來寫日誌,根本沒有起到線程的優勢。因爲我們在一個線程阻塞的時候,並沒有辦法啓動另一個線程!!而是等到這個阻塞完畢後,再調用另一個,這樣和順序執行就沒有任何差別了。我們要的是及時喚醒另一個寫進程(我們知道,這樣帶來的後果就是使得日誌開始錯亂,分不清是第一個寫的還是第二個寫的,甚至沒有正常的語法!)

我們該怎麼辦?

好吧,這一篇已經夠長了,大家估計已經沒有耐性讀下去了。。。

但是還有很多問題沒解決,還有很多不清楚的地方,不是麼?OK,在下一篇文章中,我們將繼續對C#中的線程做進一步探究。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章