在多線程訪問讀寫同一個文件時,經常遇到異常:“文件正在由另一進程使用,因此該進程無法訪問此文件”。
多線程訪問統一資源的異常,
解決方案1,保證讀寫操作單線程執行,可以使用lock
解決方案2,使用System.Threading.ReaderWriterLockSlim ,對讀寫操作鎖定處理
讀寫鎖是以 ReaderWriterLockSlim 對象作為鎖管理資源的,不同的 ReaderWriterLockSlim 對象中鎖定同一個文件也會被視為不同的鎖進行管理,這種差異可能會再次導致文件的並發寫入問題,所以 ReaderWriterLockSlim 應盡量定義為只讀的靜態對象。
ReaderWriterLockSlim 有幾個關鍵的方法,本文僅討論寫入鎖:
調用 EnterWriteLock 方法 進入寫入狀態,在調用線程進入鎖定狀態之前一直處於阻塞狀態,因此可能永遠都不返回。
調用 TryEnterWriteLock 方法 進入寫入狀態,可指定阻塞的間隔時間,如果調用線程在此間隔期間並未進入寫入模式,將返回false。
調用 ExitWriteLock 方法 退出寫入狀態,應使用 finally 塊執行 ExitWriteLock 方法,從而確保調用方退出寫入模式。
一、不是用鎖處理,多線程訪問文件不定時拋出異常
static void Main(string[] args) { //迭代運行寫入日志記錄,由於多個線程同時寫入同一個文件將會導致錯誤 Parallel.For(0, LogCount, e => { WriteLog(); }); Console.Read(); } static int LogCount = 100; static int FailedCount = 0; static int WriteCount = 0; static void WriteLog() { try { WriteCount++; LogHelper.LogHelper _log = new LogHelper.LogHelper("g:\\temp2\\one.txt", true); DateTime now = DateTime.Now; var logContent = string.Format("Tid: {0}{1} {2}=>{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), WriteCount); _log.WriteLine(logContent); } catch (Exception ex) { FailedCount++; Console.WriteLine("累計出錯數:" + FailedCount); Console.WriteLine(ex.Message); } }
二、使用讀寫鎖 同步寫入文件處理
//讀寫鎖,當資源處於寫入模式時,其他線程寫入需要等待本次寫入結束之后才能繼續寫入 static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); static void WriteLog() { try { //設置讀寫鎖為寫入模式獨占資源,其他寫入請求需要等待本次寫入結束之后才能繼續寫入 //注意:長時間持有讀線程鎖或寫線程鎖會使其他線程發生飢餓 (starve)。 為了得到最好的性能,需要考慮重新構造應用程序以將寫訪問的持續時間減少到最小。 //從性能方面考慮,請求進入寫入模式應該緊跟文件操作之前,在此處進入寫入模式僅是為了降低代碼復雜度 //因進入與退出寫入模式應在同一個try finally語句塊內,所以在請求進入寫入模式之前不能觸發異常,否則釋放次數大於請求次數將會觸發異常 LogWriteLock.EnterWriteLock(); WriteCount++; LogHelper.LogHelper _log = new LogHelper.LogHelper("g:\\temp2\\one.txt", true); DateTime now = DateTime.Now; var logContent = string.Format("Tid: {0}{1} {2}=>{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), WriteCount); _log.WriteLine(logContent); } catch (Exception ex) { FailedCount++; Console.WriteLine("累計出錯數:" + FailedCount); Console.WriteLine(ex.Message); } finally { //退出寫入模式,釋放資源占用 //注意:一次請求對應一次釋放 //若釋放次數大於請求次數將會觸發異常[寫入鎖定未經保持即被釋放] //若請求處理完成后未釋放將會觸發異常[此模式不下允許以遞歸方式獲取寫入鎖定] LogWriteLock.ExitWriteLock(); } }
三、補充:初始化FileStream時使用包含文件共享屬性(System.IO.FileShare)的構造函數比使用自定義線程鎖更為安全和高效
1 class Program 2 { 3 static int LogCount = 100; 4 static int WritedCount = 0; 5 static int FailedCount = 0; 6 7 static void Main(string[] args) 8 { 9 //迭代運行寫入日志記錄 10 Parallel.For(0, LogCount, e => 11 { 12 WriteLog(); 13 }); 14 15 Console.WriteLine(string.Format("\r\nLog Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString())); 16 Console.Read(); 17 } 18 19 static void WriteLog() 20 { 21 try 22 { 23 var logFilePath = "log.txt"; 24 var now = DateTime.Now; 25 var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString()); 26 27 var logContentBytes = Encoding.Default.GetBytes(logContent); 28 //由於設置了文件共享模式為允許隨后寫入,所以即使多個線程同時寫入文件,也會等待之前的線程寫入結束之后再執行,而不會出現錯誤 29 using (FileStream logFile = new FileStream(logFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write)) 30 { 31 logFile.Seek(0, SeekOrigin.End); 32 logFile.Write(logContentBytes, 0, logContentBytes.Length); 33 } 34 35 WritedCount++; 36 } 37 catch (Exception ex) 38 { 39 FailedCount++; 40 Console.WriteLine(ex.Message); 41 } 42 } 43 }
更多: