1. 知識儲備
在開發(fā)程序的過程中,難免少不了寫入錯誤日志這個關(guān)鍵功能。實現(xiàn)這個功能另一個程序正在使用此文件 進(jìn)程無法訪問,可以選擇使用第三方日志插件,也可以選擇使用數(shù)據(jù)庫,還可以自己寫個簡單的方法把錯誤信息記錄到日志文件。
選擇最后一種方法實現(xiàn)的時候,若對文件操作與線程同步不熟悉,問題就有可能出現(xiàn)了另一個程序正在使用此文件 進(jìn)程無法訪問,因為同一個文件并不允許多個線程同時寫入,否則會提示“文件正在由另一進(jìn)程使用,因此該進(jìn)程無法訪問此文件”。
這是文件的并發(fā)寫入問題,就需要用到線程同步。而微軟也給線程同步提供了一些相關(guān)的類可以達(dá)到這樣的目的,本文使用到的 .. 便是其中之一。
該類用于管理資源訪問的鎖定狀態(tài),可實現(xiàn)多線程讀取或進(jìn)行獨(dú)占式寫入訪問。利用這個類,我們就可以避免在同一時間段內(nèi)多線程同時寫入一個文件而導(dǎo)致的并發(fā)寫入問題。
讀寫鎖是以 對象作為鎖管理資源的,不同的 對象中鎖定同一個文件也會被視為不同的鎖進(jìn)行管理,這種差異可能會再次導(dǎo)致文件的并發(fā)寫入問題,所以 應(yīng)盡量定義為只讀的靜態(tài)對象。
有幾個關(guān)鍵的方法,本文僅討論寫入鎖:
2. 多線程同時寫入文件
class?Program
{
????static?int?LogCount?=?100;
????static?int?WritedCount?=?0;
????static?int?FailedCount?=?0;
????static?void?Main(string[]?args)
????{
????????//迭代運(yùn)行寫入日志記錄,由于多個線程同時寫入同一個文件將會導(dǎo)致錯誤
????????Parallel.For(0,?LogCount,?e?=>
????????{
????????????WriteLog();
????????});
????????Console.WriteLine(string.Format("\r\nLog?Count:{0}.\t\tWrited?Count:{1}.\tFailed?Count:{2}.",?LogCount.ToString(),?WritedCount.ToString(),?FailedCount.ToString()));
????????Console.Read();
????}
????static?void?WriteLog()
????{
????????try
????????{
????????????var?logFilePath?=?"log.txt";
????????????var?now?=?DateTime.Now;
????????????var?logContent?=?string.Format("Tid:?{0}{1}?{2}.{3}\r\n",?Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4),?now.ToLongDateString(),?now.ToLongTimeString(),?now.Millisecond.ToString());
????????????File.AppendAllText(logFilePath,?logContent);
????????????WritedCount++;
????????}
????????catch?(Exception?ex)
????????{
????????????FailedCount++;
????????????Console.WriteLine(ex.Message);
????????}
????}
}
3. 多線程使用讀寫鎖同步寫入文件
class?Program
{
????static?int?LogCount?=?100;
????static?int?WritedCount?=?0;
????static?int?FailedCount?=?0;
????static?void?Main(string[]?args)
????{
????????//迭代運(yùn)行寫入日志記錄
????????Parallel.For(0,?LogCount,?e?=>
????????{
????????????WriteLog();
????????});
????????Console.WriteLine(string.Format("\r\nLog?Count:{0}.\t\tWrited?Count:{1}.\tFailed?Count:{2}.",?LogCount.ToString(),?WritedCount.ToString(),?FailedCount.ToString()));
????????Console.Read();
????}
????//讀寫鎖,當(dāng)資源處于寫入模式時,其他線程寫入需要等待本次寫入結(jié)束之后才能繼續(xù)寫入
????static?ReaderWriterLockSlim?LogWriteLock?=?new?ReaderWriterLockSlim();
????static?void?WriteLog()
????{
????????try
????????{
????????????//設(shè)置讀寫鎖為寫入模式獨(dú)占資源,其他寫入請求需要等待本次寫入結(jié)束之后才能繼續(xù)寫入
????????????//注意:長時間持有讀線程鎖或?qū)懢€程鎖會使其他線程發(fā)生饑餓?(starve)。?為了得到最好的性能,需要考慮重新構(gòu)造應(yīng)用程序以將寫訪問的持續(xù)時間減少到最小。
????????????//??????從性能方面考慮,請求進(jìn)入寫入模式應(yīng)該緊跟文件操作之前,在此處進(jìn)入寫入模式僅是為了降低代碼復(fù)雜度
????????????//??????因進(jìn)入與退出寫入模式應(yīng)在同一個try?finally語句塊內(nèi),所以在請求進(jìn)入寫入模式之前不能觸發(fā)異常,否則釋放次數(shù)大于請求次數(shù)將會觸發(fā)異常
????????????LogWriteLock.EnterWriteLock();
????????????var?logFilePath?=?"log.txt";
????????????var?now?=?DateTime.Now;
????????????var?logContent?=?string.Format("Tid:?{0}{1}?{2}.{3}\r\n",?Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4),?now.ToLongDateString(),?now.ToLongTimeString(),?now.Millisecond.ToString());
????????????File.AppendAllText(logFilePath,?logContent);
????????????WritedCount++;
????????}
????????catch?(Exception)
????????{
????????????FailedCount++;
????????}
????????finally
????????{
????????????//退出寫入模式,釋放資源占用
????????????//注意:一次請求對應(yīng)一次釋放
????????????//??????若釋放次數(shù)大于請求次數(shù)將會觸發(fā)異常[寫入鎖定未經(jīng)保持即被釋放]
????????????//??????若請求處理完成后未釋放將會觸發(fā)異常[此模式下允許以遞歸方式獲取寫入鎖定]
????????????LogWriteLock.ExitWriteLock();
????????}
????}
}
使用讀寫鎖,全部日志成功寫入了日志文件。
4. 測試復(fù)雜多線程環(huán)境下使用讀寫鎖同步寫入文件
class?Program
{
????static?int?LogCount?=?1000;
????static?int?SumLogCount?=?0;
????static?int?WritedCount?=?0;
????static?int?FailedCount?=?0;
????static?void?Main(string[]?args)
????{
????????//往線程池里添加一個任務(wù),迭代寫入N個日志
????????SumLogCount?+=?LogCount;
????????ThreadPool.QueueUserWorkItem((obj)?=>
????????{
????????????Parallel.For(0,?LogCount,?e?=>
????????????{
????????????????WriteLog();
????????????});
????????});
????????//在新的線程里,添加N個寫入日志的任務(wù)到線程池
????????SumLogCount?+=?LogCount;
????????var?thread1?=?new?Thread(()?=>
????????{
????????????Parallel.For(0,?LogCount,?e?=>
????????????{
????????????????ThreadPool.QueueUserWorkItem((subObj)?=>
????????????????{
????????????????????WriteLog();
????????????????});
????????????});
????????});
????????thread1.IsBackground?=?false;
????????thread1.Start();
????????//添加N個寫入日志的任務(wù)到線程池
????????SumLogCount?+=?LogCount;
????????Parallel.For(0,?LogCount,?e?=>
????????{
????????????ThreadPool.QueueUserWorkItem((obj)?=>
????????????{
????????????????WriteLog();
????????????});
????????});
????????//在新的線程里,迭代寫入N個日志
????????SumLogCount?+=?LogCount;
????????var?thread2?=?new?Thread(()?=>
????????{
????????????Parallel.For(0,?LogCount,?e?=>
????????????{
????????????????WriteLog();
????????????});
????????});
????????thread2.IsBackground?=?false;
????????thread2.Start();
????????//在當(dāng)前線程里,迭代寫入N個日志
????????SumLogCount?+=?LogCount;
????????Parallel.For(0,?LogCount,?e?=>
????????{
????????????WriteLog();
????????});
????????Console.WriteLine("Main?Thread?Processed.\r\n");
????????while?(true)
????????{
????????????Console.WriteLine(string.Format("Sum?Log?Count:{0}.\t\tWrited?Count:{1}.\tFailed?Count:{2}.",?SumLogCount.ToString(),?WritedCount.ToString(),?FailedCount.ToString()));
????????????Console.ReadLine();
????????}
????}
????//讀寫鎖,當(dāng)資源處于寫入模式時,其他線程寫入需要等待本次寫入結(jié)束之后才能繼續(xù)寫入
????static?ReaderWriterLockSlim?LogWriteLock?=?new?ReaderWriterLockSlim();
????static?void?WriteLog()
????{
????????try
????????{
????????????//設(shè)置讀寫鎖為寫入模式獨(dú)占資源,其他寫入請求需要等待本次寫入結(jié)束之后才能繼續(xù)寫入
????????????//注意:長時間持有讀線程鎖或?qū)懢€程鎖會使其他線程發(fā)生饑餓?(starve)。?為了得到最好的性能,需要考慮重新構(gòu)造應(yīng)用程序以將寫訪問的持續(xù)時間減少到最小。
????????????//??????從性能方面考慮,請求進(jìn)入寫入模式應(yīng)該緊跟文件操作之前,在此處進(jìn)入寫入模式僅是為了降低代碼復(fù)雜度
????????????//??????因進(jìn)入與退出寫入模式應(yīng)在同一個try?finally語句塊內(nèi),所以在請求進(jìn)入寫入模式之前不能觸發(fā)異常,否則釋放次數(shù)大于請求次數(shù)將會觸發(fā)異常
????????????LogWriteLock.EnterWriteLock();
????????????var?logFilePath?=?"log.txt";
????????????var?now?=?DateTime.Now;
????????????var?logContent?=?string.Format("Tid:?{0}{1}?{2}.{3}\r\n",?Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4),?now.ToLongDateString(),?now.ToLongTimeString(),?now.Millisecond.ToString());
????????????File.AppendAllText(logFilePath,?logContent);
????????????WritedCount++;
????????}
????????catch?(Exception)
????????{
????????????FailedCount++;
????????}
????????finally
????????{
????????????//退出寫入模式,釋放資源占用
????????????//注意:一次請求對應(yīng)一次釋放
????????????//??????若釋放次數(shù)大于請求次數(shù)將會觸發(fā)異常[寫入鎖定未經(jīng)保持即被釋放]
????????????//??????若請求處理完成后未釋放將會觸發(fā)異常[此模式不下允許以遞歸方式獲取寫入鎖定]
????????????LogWriteLock.ExitWriteLock();
????????}
????}
}
復(fù)雜多線程環(huán)境下使用讀寫鎖,全部日志成功寫入了日志文件,由和可以看出是由不同的線程同步寫入。
5. 后記
雖然讀寫IO有共享模式,也可以實現(xiàn)但不推薦
static?void?WriteLog()
{
????try
????{
????????var?logFilePath?=?"log.txt";
????????var?now?=?DateTime.Now;
????????var?logContent?=?string.Format("Tid:?{0}{1}?{2}.{3}\r\n",?Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4),?now.ToLongDateString(),?now.ToLongTimeString(),?now.Millisecond.ToString());
????????//File.AppendAllText(logFilePath,?logContent);
?
????????var?logContentBytes?=?Encoding.Default.GetBytes(logContent);
????????using?(FileStream?logFile?=?new?FileStream(logFilePath,?FileMode.OpenOrCreate,?FileAccess.Write,?FileShare.Write))
????????{
????????????logFile.Seek(0,?SeekOrigin.End);
????????????logFile.Write(logContentBytes,?0,?logContentBytes.Length);
????????}
?
????????WritedCount++;
????}
????catch?(Exception?ex)
????{
????????FailedCount++;
????????Console.WriteLine(ex.Message);
????}
}