一,您選擇用什么樣的日志組件
日志組件,不得不提大名鼎鼎的Log4Net。比較常用的還有 Enterprise Library Logging,ServiceStack Logging。當然您還可以補充,我就只用過這幾款。
上邊提到的3款日志組件,都要在.config里加代碼,特別是Log4Net,還要把SQL寫在配置里。我就是僅僅只寫個日志,還要配置這么多信息,讓人略有不爽。
所以在很長一段時間里,我用下邊這個方法寫日志:

private static void WriteText(string logPath, string logContent) { try { if (!File.Exists(logPath)) { File.CreateText(logPath).Close(); } StreamWriter sw = File.AppendText(logPath); sw.Write(logContent); sw.Close(); } catch (Exception ex) { } finally { } }
這個方法足夠的簡單,核心代碼就只有那么5,6行,還包含容錯機制。我就喜歡用這種簡單的代碼來處理簡單的事。
二,多線程下引爆了問題
在多線程的情況下,比如100個線程同時需要寫日志,上邊提到的這個方法就力不從心了。
一個線程訪問日志資源,另一個線程再去訪問的時候,就會出現異常。
方法一:
public static Object _processLock = new Object(); private void Button_Click_1(object sender, RoutedEventArgs e) { lock (_processLock) { } }
方法二:
public static Object _processLock = new Object(); private void Button_Click_1(object sender, RoutedEventArgs e) { Monitor.Enter(_processLock); Monitor.Exit(_processLock); }
這樣,你不得不承認,我已經解決了多線程的問題。
但是有瓶頸,這些需要寫日志的線程,必須要等前一個釋放了鎖資源,后一個線程才能訪問的情況。
三,重新設計日志組件
先看圖,再說一下我的思路:
1,不管有多少線程同時需要寫日志,我都用一個臨時隊列來存放這些日志信息。
2,再啟用一個Task任務把隊列的日志批量存放到.log文件里。
3,附加一個小功能,每個日志存儲的大小限制,當日志太大了,查看打開的時候比較慢。
四,具體的代碼實現
1,在多線程的情況下,我們首先把日志壓到Queue隊列里
static ConcurrentQueue<Tuple<string, string>> logQueue = new ConcurrentQueue<Tuple<string, string>>();
在這兒,我為什么選用 ConcurrentQueue 而不是 Queue 。因為ConcurrentQueue 表示線程安全的先進先出 (FIFO) 集合。
當然你一定要用Queue也是可以的,但是要自己去實現鎖機制,何必自找麻煩呢?
2,把日志隊列里的數據,批量持久化到.log文件里
這個問題,讓我很頭大。我最開始的方法是:
持久化日志方法一:
a,申明一個Task任務,當我Task任務沒有實現化時,先實例化,然后再進行持久化日志寫入。
b,當我的Task任務,已經實例化了,並且是處於 IsCompleted 狀態,我重新實例化Task,再進行持久化日志的寫入。
static Task writeTask = default(Task);

public static void WriteLog(String customDirectory, String preFile, String infoData) { string logPath = GetLogPath(customDirectory, preFile); string logContent = String.Concat(DateTime.Now, " ", infoData); logQueue.Enqueue(new Tuple<string, string>(logPath, logContent)); if (writeTask == null) { writeTask = new Task((object obj) => { //pause.WaitOne(1000, true); LogRepository(); } , null , TaskCreationOptions.LongRunning); writeTask.Start(); } if (writeTask.IsCompleted) { writeTask = new Task((object obj) => { //pause.WaitOne(1000, true); LogRepository(); } , null , TaskCreationOptions.LongRunning); writeTask.Start(); } }
異常信息:
理論是那么的美好,但是現實是那么殘酷,當我跑單元測試的時候,一段時間后總是拋出如下錯誤。如果是有那位朋友知道其原因,把這個問題解決就完美了。
但是我不能因為這一個異常,導致我這個組件寫不下去吧!活人不能被一泡尿給憋死。
追加:完整代碼如下:

public class IOExtention { static ConcurrentQueue<Tuple<string, string>> logQueue = new ConcurrentQueue<Tuple<string, string>>(); static volatile Task writeTask = default(Task); static IOExtention() { } public static void WriteLog(String preFile, String infoData) { WriteLog(string.Empty, preFile, infoData); } static AutoResetEvent pause = new AutoResetEvent(false); public static void WriteLog(String customDirectory, String preFile, String infoData) { string logPath = GetLogPath(customDirectory, preFile); string logContent = String.Concat(DateTime.Now, " ", infoData); logQueue.Enqueue(new Tuple<string, string>(logPath, logContent)); if (writeTask == null) { writeTask = new Task((object obj) => { //pause.WaitOne(1000, true); LogRepository(); } , null , TaskCreationOptions.LongRunning); writeTask.Start(); } if (writeTask.IsCompleted) { writeTask = new Task((object obj) => { //pause.WaitOne(1000, true); LogRepository(); } , null , TaskCreationOptions.LongRunning); writeTask.Start(); } } public static void LogRepository() { List<string[]> temp = new List<string[]>(); foreach (var logItem in logQueue) { string logPath = logItem.Item1; string logMergeContent = String.Concat(logItem.Item2, Environment.NewLine, "-----------------------------------------------------------", Environment.NewLine); string[] logArr = temp.FirstOrDefault(d => d[0].Equals(logPath)); if (logArr != null) { logArr[1] = string.Concat(logArr[1], logMergeContent); } else { logArr = new string[] { logPath, logMergeContent }; temp.Add(logArr); } Tuple<string, string> val = default(Tuple<string, string>); logQueue.TryDequeue(out val); } foreach (string[] item in temp) { WriteText(item[0], item[1]); } } private static string GetLogPath(String customDirectory, String preFile) { string newFilePath = string.Empty; String logDir = string.IsNullOrEmpty(customDirectory) ? Path.Combine(Environment.CurrentDirectory, "logs") : customDirectory; if (!Directory.Exists(logDir)) { Directory.CreateDirectory(logDir); } string extension = ".log"; string fileNameNotExt = String.Concat(preFile, DateTime.Now.ToString("yyyyMMdd")); String fileName = String.Concat(fileNameNotExt, extension); string fileNamePattern = string.Concat(fileNameNotExt, "(*)", extension); List<string> filePaths = Directory.GetFiles(logDir, fileNamePattern, SearchOption.TopDirectoryOnly).ToList(); if (filePaths.Count > 0) { int fileMaxLen = filePaths.Max(d => d.Length); string lastFilePath = filePaths.Where(d => d.Length == fileMaxLen).OrderByDescending(d => d).FirstOrDefault(); if (new FileInfo(lastFilePath).Length > 1 * 1024 * 1024) { string no = new Regex(@"(?is)(?<=\()(.*)(?=\))").Match(Path.GetFileName(lastFilePath)).Value; int tempno = 0; bool parse = int.TryParse(no, out tempno); string formatno = String.Format("({0})", parse ? (tempno + 1) : tempno); string newFileName = String.Concat(fileNameNotExt, formatno, extension); newFilePath = Path.Combine(logDir, newFileName); } else { newFilePath = lastFilePath; } } else { string newFileName = String.Concat(fileNameNotExt, String.Format("({0})", 0), extension); newFilePath = Path.Combine(logDir, newFileName); } return newFilePath; } private static void WriteText(string logPath, string logContent) { try { if (!File.Exists(logPath)) { File.CreateText(logPath).Close(); } StreamWriter sw = File.AppendText(logPath); sw.Write(logContent); sw.Close(); } catch (Exception ex) { } finally { } } }
持久化日志方法二:
我采用了另外一種方法,在Task任務里我用信號量的方式來解決了些問題,完整代碼如下:
static AutoResetEvent pause = new AutoResetEvent(false);
信號量法:

public class IOExtention { static ConcurrentQueue<Tuple<string, string>> logQueue = new ConcurrentQueue<Tuple<string, string>>(); static Task writeTask = default(Task); static IOExtention() { writeTask = new Task((object obj) => { while (true) { pause.WaitOne(1000, true); List<string[]> temp = new List<string[]>(); foreach (var logItem in logQueue) { string logPath = logItem.Item1; string logMergeContent = String.Concat(logItem.Item2, Environment.NewLine, "-----------------------------------------------------------", Environment.NewLine); string[] logArr = temp.FirstOrDefault(d => d[0].Equals(logPath)); if (logArr != null) { logArr[1] = string.Concat(logArr[1], logMergeContent); } else { logArr = new string[] { logPath, logMergeContent }; temp.Add(logArr); } Tuple<string, string> val = default(Tuple<string, string>); logQueue.TryDequeue(out val); } foreach (string[] item in temp) { WriteText(item[0], item[1]); } } } , null , TaskCreationOptions.LongRunning); writeTask.Start(); } public static void WriteLog(String preFile, String infoData) { WriteLog(string.Empty, preFile, infoData); } static AutoResetEvent pause = new AutoResetEvent(false); public static void WriteLog(String customDirectory, String preFile, String infoData) { string logPath = GetLogPath(customDirectory, preFile); string logContent = String.Concat(DateTime.Now, " ", infoData); logQueue.Enqueue(new Tuple<string, string>(logPath, logContent)); } private static string GetLogPath(String customDirectory, String preFile) { string newFilePath = string.Empty; String logDir = string.IsNullOrEmpty(customDirectory) ? Path.Combine(Environment.CurrentDirectory, "logs") : customDirectory; if (!Directory.Exists(logDir)) { Directory.CreateDirectory(logDir); } string extension = ".log"; string fileNameNotExt = String.Concat(preFile, DateTime.Now.ToString("yyyyMMdd")); String fileName = String.Concat(fileNameNotExt, extension); string fileNamePattern = string.Concat(fileNameNotExt, "(*)", extension); List<string> filePaths = Directory.GetFiles(logDir, fileNamePattern, SearchOption.TopDirectoryOnly).ToList(); if (filePaths.Count > 0) { int fileMaxLen = filePaths.Max(d => d.Length); string lastFilePath = filePaths.Where(d => d.Length == fileMaxLen).OrderByDescending(d => d).FirstOrDefault(); if (new FileInfo(lastFilePath).Length > 1 * 1024 * 1024) { string no = new Regex(@"(?is)(?<=\()(.*)(?=\))").Match(Path.GetFileName(lastFilePath)).Value; int tempno = 0; bool parse = int.TryParse(no, out tempno); string formatno = String.Format("({0})", parse ? (tempno + 1) : tempno); string newFileName = String.Concat(fileNameNotExt, formatno, extension); newFilePath = Path.Combine(logDir, newFileName); } else { newFilePath = lastFilePath; } } else { string newFileName = String.Concat(fileNameNotExt, String.Format("({0})", 0), extension); newFilePath = Path.Combine(logDir, newFileName); } return newFilePath; } private static void WriteText(string logPath, string logContent) { try { if (!File.Exists(logPath)) { File.CreateText(logPath).Close(); } StreamWriter sw = File.AppendText(logPath); sw.Write(logContent); sw.Close(); } catch (Exception ex) { } finally { } } }
持久化日志方法三:
如果你感覺寫一個日志類還用什么信號量這些技術,太復雜了,那也可以用最簡單的方式,定時器來解決。
有同學一聽定時器,就默默的笑了,但是這兒的坑也很深,首先了解一下這幾個定時器的使用場合,再用不遲!
System.Windows.Threading.DispatcherTimer
System.Windows.Forms.Timer
System.Timers.Timer
System.Threading.Timer
定時器法:

public class IOExtention { static ConcurrentQueue<Tuple<string, string>> logQueue = new ConcurrentQueue<Tuple<string, string>>(); static System.Timers.Timer logTimers = new System.Timers.Timer(); static IOExtention() { logTimers.Interval = 1000; logTimers.Elapsed += logTimers_Elapsed; logTimers.AutoReset = true; logTimers.Enabled = true; } public static void WriteLog(String preFile, String infoData) { WriteLog(string.Empty, preFile, infoData); } public static void WriteLog(String customDirectory, String preFile, String infoData) { string logPath = GetLogPath(customDirectory, preFile); string logContent = String.Concat(DateTime.Now, " ", infoData); logQueue.Enqueue(new Tuple<string, string>(logPath, logContent)); } private static string GetLogPath(String customDirectory, String preFile) { string newFilePath = string.Empty; String logDir = string.IsNullOrEmpty(customDirectory) ? Path.Combine(Environment.CurrentDirectory, "logs") : customDirectory; if (!Directory.Exists(logDir)) { Directory.CreateDirectory(logDir); } string extension = ".log"; string fileNameNotExt = String.Concat(preFile, DateTime.Now.ToString("yyyyMMdd")); String fileName = String.Concat(fileNameNotExt, extension); string fileNamePattern = string.Concat(fileNameNotExt, "(*)", extension); List<string> filePaths = Directory.GetFiles(logDir, fileNamePattern, SearchOption.TopDirectoryOnly).ToList(); if (filePaths.Count > 0) { int fileMaxLen = filePaths.Max(d => d.Length); string lastFilePath = filePaths.Where(d => d.Length == fileMaxLen).OrderByDescending(d => d).FirstOrDefault(); if (new FileInfo(lastFilePath).Length > 1 * 1024 * 1024) { string no = new Regex(@"(?is)(?<=\()(.*)(?=\))").Match(Path.GetFileName(lastFilePath)).Value; int tempno = 0; bool parse = int.TryParse(no, out tempno); string formatno = String.Format("({0})", parse ? (tempno + 1) : tempno); string newFileName = String.Concat(fileNameNotExt, formatno, extension); newFilePath = Path.Combine(logDir, newFileName); } else { newFilePath = lastFilePath; } } else { string newFileName = String.Concat(fileNameNotExt, String.Format("({0})", 0), extension); newFilePath = Path.Combine(logDir, newFileName); } return newFilePath; } static void logTimers_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { System.Timers.Timer logTimers = (System.Timers.Timer)sender; logTimers.Enabled = false; List<string[]> temp = new List<string[]>(); foreach (var logItem in logQueue) { string logPath = logItem.Item1; string logMergeContent = String.Concat(logItem.Item2, Environment.NewLine, "-----------------------------------------------------------", Environment.NewLine); string[] logArr = temp.FirstOrDefault(d => d[0].Equals(logPath)); if (logArr != null) { logArr[1] = string.Concat(logArr[1], logMergeContent); } else { logArr = new string[] { logPath, logMergeContent }; temp.Add(logArr); } Tuple<string, string> val = default(Tuple<string, string>); logQueue.TryDequeue(out val); } foreach (string[] item in temp) { WriteText(item[0], item[1]); } logTimers.Enabled = true; } private static void WriteText(string logPath, string logContent) { try { if (!File.Exists(logPath)) { File.CreateText(logPath).Close(); } StreamWriter sw = File.AppendText(logPath); sw.Write(logContent); sw.Close(); } catch (Exception ex) { } finally { } } }
五,結語
重新設計的日志組件,思路還是非常清晰的。只是在持久化日志時遇上了問題了。
持久化日志方法一,其實是很完美的解決方法,但是在高並發的時候,總是拋出異常,找不出原因。
持久化日志方法二,是我目前采用的方法,能夠有效的解決問題。
持久化日志方法三,采用定時器解決,也是可行的。只是代碼看起來很別扭。
歡迎大家熱烈討論,看有沒有更好的解決方案。
阿里雲客戶端的實現(支持文件分塊,斷點續傳,進度,速度,倒計時顯示)
信號量改進版:

using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace LogTest { public class IOExtention { static ConcurrentQueue<Tuple<string, string>> logQueue = new ConcurrentQueue<Tuple<string, string>>(); static Task writeTask = default(Task); static ManualResetEvent pause = new ManualResetEvent(false); //Mutex mmm = new Mutex(); static IOExtention() { writeTask = new Task((object obj) => { while (true) { pause.WaitOne(); pause.Reset(); List<string[]> temp = new List<string[]>(); foreach (var logItem in logQueue) { string logPath = logItem.Item1; string logMergeContent = String.Concat(logItem.Item2, Environment.NewLine, "-----------------------------------------------------------", Environment.NewLine); string[] logArr = temp.FirstOrDefault(d => d[0].Equals(logPath)); if (logArr != null) { logArr[1] = string.Concat(logArr[1], logMergeContent); } else { logArr = new string[] { logPath, logMergeContent }; temp.Add(logArr); } Tuple<string, string> val = default(Tuple<string, string>); logQueue.TryDequeue(out val); } foreach (string[] item in temp) { WriteText(item[0], item[1]); } } } , null , TaskCreationOptions.LongRunning); writeTask.Start(); } public static void WriteLog(String preFile, String infoData) { WriteLog(string.Empty, preFile, infoData); } public static void WriteLog(String customDirectory, String preFile, String infoData) { string logPath = GetLogPath(customDirectory, preFile); string logContent = String.Concat(DateTime.Now, " ", infoData); logQueue.Enqueue(new Tuple<string, string>(logPath, logContent)); pause.Set(); } private static string GetLogPath(String customDirectory, String preFile) { string newFilePath = string.Empty; String logDir = string.IsNullOrEmpty(customDirectory) ? Path.Combine(Environment.CurrentDirectory, "logs") : customDirectory; if (!Directory.Exists(logDir)) { Directory.CreateDirectory(logDir); } string extension = ".log"; string fileNameNotExt = String.Concat(preFile, DateTime.Now.ToString("yyyyMMdd")); String fileName = String.Concat(fileNameNotExt, extension); string fileNamePattern = string.Concat(fileNameNotExt, "(*)", extension); List<string> filePaths = Directory.GetFiles(logDir, fileNamePattern, SearchOption.TopDirectoryOnly).ToList(); if (filePaths.Count > 0) { int fileMaxLen = filePaths.Max(d => d.Length); string lastFilePath = filePaths.Where(d => d.Length == fileMaxLen).OrderByDescending(d => d).FirstOrDefault(); if (new FileInfo(lastFilePath).Length > 1 * 1024 * 1024 * 1024) { string no = new Regex(@"(?is)(?<=\()(.*)(?=\))").Match(Path.GetFileName(lastFilePath)).Value; int tempno = 0; bool parse = int.TryParse(no, out tempno); string formatno = String.Format("({0})", parse ? (tempno + 1) : tempno); string newFileName = String.Concat(fileNameNotExt, formatno, extension); newFilePath = Path.Combine(logDir, newFileName); } else { newFilePath = lastFilePath; } } else { string newFileName = String.Concat(fileNameNotExt, String.Format("({0})", 0), extension); newFilePath = Path.Combine(logDir, newFileName); } return newFilePath; } private static void WriteText(string logPath, string logContent) { try { if (!File.Exists(logPath)) { File.CreateText(logPath).Close(); } StreamWriter sw = File.AppendText(logPath); sw.Write(logContent); sw.Close(); } catch (Exception ex) { } finally { } } } }