一、簡介html
本文主要演示平常開發中利用多線程寫入文件存在的問題,以及解決方案,本文使用最經常使用的日誌案例!安全
二、使用File.AppendAllText寫入日誌多線程
這是種常規的作法,經過File定位到日誌文件所在位置,而後寫入相應的日誌內容,代碼以下:異步
static string _filePath = @"C:\Users\zhengchao\Desktop\測試文件.txt"; static void Main(string[] args) { WriteLogAsync(); Console.ReadKey(); } static void WriteLogAsync() { var logRequestNum = 100000;//請求寫入日誌次數 var successCount =0;//執行成功次數 var failCount = 0;//執行失敗次數 //模擬100000次用戶請求寫入日誌操做 Parallel.For(0, logRequestNum, i => { try { var now = DateTime.Now; var logContent = $"當前線程Id:{Thread.CurrentThread.ManagedThreadId},日誌內容:暫時沒有,日誌級別:Warn,寫入時間:{now.ToString()}"; File.AppendAllText(_filePath, logContent); successCount++; } catch (Exception ex) { failCount++; Console.WriteLine(ex.Message); } }); Console.WriteLine($"Request Count:{logRequestNum}. Success Count:{successCount} Failed Count:{failCount}."); }
報錯了,緣由,Windows不容許多個線程同時操做同一個文件,因此,拋異常.因此必須解決這個問題。測試
三、利用ReadWriterSlim解決多線程徵用文件問題優化
關於ReadWriterSlim的使用,在本人的這篇隨筆中已介紹,在其基礎上,對SynchronizedCache類稍稍改造,造成一個SynchronizedFile類,對相關操做代碼進行線程安全處理,即能解決當前的問題,代碼以下:spa
public class SynchronizedFile { private static ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim(); /// <summary> /// 線程安全的寫入文件操做 /// </summary> /// <param name="action"></param> public static void WriteFile(Action action) { cacheLock.EnterWriteLock(); try { action.Invoke(); } finally { cacheLock.ExitWriteLock(); } } }
調用代碼以下所示:pwa
static string _filePath = @"C:\Users\zhengchao\Desktop\測試文件.txt"; static void Main(string[] args) { WriteLogSync(); Console.ReadKey(); } /// <summary> /// 多線程同步寫入文件 /// </summary> static void WriteLogSync() { var logRequestNum = 10000;//請求寫入日誌次數 var successCount =0;//執行成功次數 var failCount = 0;//執行失敗次數 var stopWatch = Stopwatch.StartNew(); //模擬100000次用戶請求寫入日誌操做 var result=Parallel.For(0, logRequestNum, i => { SynchronizedFile.WriteFile(() => { try { var now = DateTime.Now; var logContent = $"當前線程Id:{Thread.CurrentThread.ManagedThreadId},日誌內容:暫時沒有,日誌級別:Warn,寫入時間:{now.ToString()}\r\n"; File.AppendAllText(_filePath, logContent); successCount++; } catch (Exception ex) { failCount++; Console.WriteLine(ex.Message); } }); }); if (result.IsCompleted) { stopWatch.Stop(); Console.WriteLine($"Request Count:{logRequestNum}. Success Count:{successCount} Failed Count:{failCount},總耗時:{stopWatch.ElapsedMilliseconds/1000}秒"); } }
內容所有寫入成功,可是尚未結束,緣由是,反編譯線程
一直反編譯下去,會發現日誌
用的是同步Api,因此代碼能夠繼續優化,同步意味着每一個線程在寫入文件時,當前的寫入托管代碼會轉換成託管代碼,最後,Windows會把當前寫入操做的數據初始化成IRP數據包傳給硬件設備,以後硬件設備開始執行寫入操做。這個過程,當前線程在和硬件交互時,不會返回到線程池,而是被Windows置爲休眠狀態,等待硬件設置執行寫入操做完畢後,接着Windows會喚起該線程,最後又回到個人託管代碼也就是C#代碼中,繼續執行下面的邏輯.因此當前的日誌寫入代碼能夠優化,使用異步Api來作.這樣當前線程不會等待硬件設備,而是返回線程池.提升CPU的利用率.
四、優化代碼
static string _filePath = @"C:\Users\zhengchao\Desktop\測試文件.txt"; static void Main(string[] args) { WriteLogAsync(); Console.ReadKey(); } /// <summary> /// 多線程異步寫入文件 /// </summary> static void WriteLogAsync() { var logRequestNum = 10000;//請求寫入日誌次數 var successCount = 0;//執行成功次數 var failCount = 0;//執行失敗次數 var stopWatch = Stopwatch.StartNew(); //模擬100000次用戶請求寫入日誌操做 var result = Parallel.For(0, logRequestNum, i => { SynchronizedFile.WriteFile(() => { try { var now = DateTime.Now; var logContent = $"當前線程Id:{Thread.CurrentThread.ManagedThreadId},日誌內容:暫時沒有,日誌級別:Warn,寫入時間:{now.ToString()}\r\n"; var utf8NoBom = new UTF8Encoding(false, true);//去掉Dom頭 using (StreamWriter writer = new StreamWriter(_filePath, true, utf8NoBom)) { writer.WriteAsync(logContent); } successCount++; } catch (Exception ex) { failCount++; Console.WriteLine(ex.Message); } }); }); if (result.IsCompleted) { stopWatch.Stop(); Console.WriteLine($"Request Count:{logRequestNum}. Success Count:{successCount} Failed Count:{failCount},總耗時:{stopWatch.ElapsedMilliseconds / 1000}秒"); } }
雖然效果差很少,可是能提高CPU利用率.暫時還沒找到多線程寫入一個文件,不須要加讀鎖的方法,若是有,請告知.