首先,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#中的線程做進一步探究。