在多執行緒存取讀寫同一個檔案時,經常遇到異常:「檔案正在由另一進程使用,因此該進程無法存取此檔案」。
多執行緒存取統一資源的異常,
解決方案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 }
更多: