100行代碼就支持了多線程并發,批量寫入日志
一,您選擇用什么樣的日志組件
日志組件,不得不提大名鼎鼎的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
{
}
}
View Code</code></pre>
這個方法足夠的簡單,核心代碼就只有那么5,6行,還包含容錯機制。我就喜歡用這種簡單的代碼來處理簡單的事。
二,多線程下引爆了問題
在多線程的情況下,比如100個線程同時需要寫日志,上邊提到的這個方法就力不從新了。
一個線程訪問日志資源,另一個線程再去訪問的時候,就會出現異常。
方法一:
public static Object _processLock = new Object();
private void Button_Click_1(object sender, RoutedEventArgs e)
{
lock (_processLock)
{
}
}
</code></pre>
方法二:
public static Object _processLock = new Object();
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Monitor.Enter(_processLock);
Monitor.Exit(_processLock);
}
</code></pre>
這樣,你不得不承認,我已經解決了多線程的問題。
但是有瓶頸,這些需要寫日志的線程,必須要等前一個釋放了鎖資源,后一個線程才能訪問的情況。
三,重新設計日志組件

先看圖,再說一下我的思路:
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();
}
}
View Code</code></pre>
異常信息:
理論是那么的美好,但是現實是那么殘酷,當我跑單元測試的時候,一段時間后總是拋出如下錯誤。如果是有那位朋友知道其原因,把這個問題解決就完美了。

但是我不能因為這一個異常,導致我這個組件寫不下去吧!活人不能被一泡尿給憋死。
持久化日志方法二:
我采用了另外一種方法,在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
{
}
}
}
View Code</code></pre>
持久化日志方法三:
如果你感覺寫一個日志類還用什么信號量這些技術,太復雜了,那也可以用最簡單的方式,定時器來解決。
有同學一聽定時器,就默默的笑了,但是這兒的坑也很深,首先了解一下這幾個定時器的使用場合,再用不遲!
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
{
}
}
}
View Code</code></pre>
五,結語
重新設計的日志組件,思路還是非常清晰的。只是在持久化日志時遇上了問題了。
持久化日志方法一,其實是很完美的解決方法,但是在高并發的時候,總是拋出異常,找不出原因。
持久化日志方法二,是我目前采用的方法,能夠有效的解決問題。
持久化日志方法三,采用定時器解決,也是可行的。只是代碼看起來很別扭。
來自:http://www.cnblogs.com/xcj26/p/6037808.html