1、簡介
本文主要演示日常開發中利用多線程寫入文件存在的問題,以及解決方案,本文使用最常用的日志案例!
2、使用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不允許多個線程同時操作同一個文件,所以,拋異常.所以必須解決這個問題。
3、利用ReadWriterSlim解決多線程征用文件問題
關於ReadWriterSlim的使用,在本人的這篇隨筆中已介紹,在其基礎上,對SynchronizedCache類稍稍改造,形成一個SynchronizedFile類,對相關操作代碼進行線程安全處理,即能解決當前的問題,代碼如下:
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(); } } }
調用代碼如下所示:
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的利用率.
4、優化代碼
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利用率.暫時還沒找到多線程寫入一個文件,不需要加讀鎖的方法,如果有,請告知.