概述
主要設計思想是通過一個共享隊列,多個輸入端能同時非阻塞式的向隊列中增加記錄信息,輸出端能自動及時的把隊列中的記錄信息輸出到控制台或是保存到文件及數據庫中。多個輸入端互相隔離,采用多線程實現,但考慮到緩存日志信息的是一個共享隊列,自然涉及到線程間的同步問題。本文的實現模式是采用操作系統中很經典的生產者/消費者模式。線程間的同步是通過事件信號,同時對共享隊列的修改進行加鎖保護,避免多個線程同時修改隊列。
日志記錄類實現
整個實現除了主要的日志記錄類,還要定義同步事件類封裝用於線程間同步的事件對象,定義日志信息類用於生成日志信息能存於共享隊列中。
1. 同步事件類 SyncEvents
該類的定義與使用參照《如何:對制造者線程和使用者線程進行同步》
public class SyncEvents { private EventWaitHandle _newItemEvent; //添加新項 private EventWaitHandle _exitThreadEvent; //退出線程 private WaitHandle[] _eventArray; public SyncEvents() { _newItemEvent = new AutoResetEvent(false); _exitThreadEvent = new ManualResetEvent(false); _eventArray = new WaitHandle[2]; _eventArray[0] = _newItemEvent; _eventArray[1] = _exitThreadEvent; } public EventWaitHandle ExitThreadEvent { get { return _exitThreadEvent; } } public EventWaitHandle NewItemEvent { get { return _newItemEvent; } } public WaitHandle[] EventArray { get { return _eventArray; } } }
對新記錄的添加使用 AutoResetEvent 類,輸出端線程在響應此事件后,此事件能自動重置。將 ManualResetEvent 類用於通知線程退出,該事件被設置后無論是向共享隊列中添加日志記錄的輸入端線程還是從共享隊列中取日志記錄的輸出端線程都能響應此事件,從而正常退出。
2. 日志信息類
共享隊列中存放的就是日志信息類的實例對象,可以根據實際需要對此類中的屬性進行增加與修改,這並不影響下面將要介紹的日志記錄類正常使用。
public class LogInfo { private int _ID; public int ID { get { return _ID; } set { _ID = value; } } private string _CreateTime; public string CreateTime { get { return _CreateTime; } set { _CreateTime = value; } } private string _Content; public string Content { get { return _Content; } set { _Content = value; } } }
3. 日志記錄類
類中屬性與構造函數
public class Logger { private static Logger _logger; private static object _lock = new object(); private static Thread _thread; //日志隊列 private Queue<LogInfo> _queue; private SyncEvents _syncEvents; private Logger() { _queue = new Queue<LogInfo>(); _syncEvents = new SyncEvents(); } //獲取日志記錄類實例 public static Logger GetLogger() { if (_logger == null) { //加鎖,防止多線程運行時,重復創建。 lock (_lock) { if (_logger == null) { _logger = new Logger(); } } } return _logger; } }
為了保證共享隊列唯一,此類實現采用了單例模式,實現方式是通過定義一個靜態的自身logger變量,私有化默認的構造函數,提供一個得到Logger實例的GetLogger方法。這樣不能通過new直接創建Logger實例,只能通過GetLogger方法獲得,在該方法中就可以通過判斷是否已創建了Logger實例,如果已創建則返回已有的,從而保證Logger實例的唯一。
添加日志方法
private void AddLog(Object obj) { LogInfo log = obj as LogInfo; if (!_syncEvents.ExitThreadEvent.WaitOne(0, false)) { lock (((ICollection)_queue).SyncRoot) { _queue.Enqueue(log); _syncEvents.NewItemEvent.Set(); Console.WriteLine("Input thread: add {0} items", log.ID); } } } /// <summary> /// 添加日志 /// </summary> /// <param name="log"></param> public void Add(LogInfo log) { Thread t = new Thread(AddLog); t.Start(log); }
首先檢查“退出線程”事件,因為 WaitOne 使用的第一個參數為零,該方法會立即返回,所以檢查該事件的狀態不會阻止當前線程。接着往共享隊列中添加日志記錄並設置“添加新項”事件,此事件設置后會讓因共享隊列為空而一直在等待的輸出線程繼續運行,處理共享隊列中的新日志記錄。 日志添加通過調用Add方法,啟動一個新線程運行AddLog方法向共享隊列中添加新日志。
日志輸出方法
/// <summary> /// 日志保存 /// </summary> private void Save() { int flag = 0; while (flag >=0 ) { if (_queue.Count == 0) { flag = WaitHandle.WaitAny(_syncEvents.EventArray); if (flag == 1) { flag = -1; } } lock (((ICollection)_queue).SyncRoot) { if (_queue.Count > 0) { LogInfo log = _queue.Dequeue(); Console.WriteLine("Output Thread: process {0} items", log.ID); } } } } public void Run() { _thread = new Thread(Save); _thread.Start(); }
輸出線程主要運行的就是日志保存方法,通過while循環逐個處理共享隊列中的日志記錄。如果隊列為空,則線程暫停進入等待狀態,等待“添加新項”事件或“退出線程”事件,兩個事件只要有一個被設置則線程繼續運行,如果是“退出線程”事件,則設置flag為-1,退出循環線程結束,因為只有在隊列為空時才等待“退出線程”事件,這樣保證線程退出前隊列中的所有的日志記錄都被處理。 在程序開始處就運行Run方法,會啟動一個新線程運行Save方法,這樣只要一添加日志就能自動的被處理。
線程結束方法
public void Stop() { _syncEvents.ExitThreadEvent.Set(); }
通過設置“退出線程”事件,讓正在運行的輸入線程和輸出線程都自動結束運行。
4. 使用示例
class Program { static void Main(string[] args) { Logger logger = Logger.GetLogger(); logger.Run(); for (int i = 0; i < 100; i++) { LogInfo log = new LogInfo(); log.ID = i; logger.Add(log); if (i == 50) { logger.Stop(); } } Console.ReadLine(); } }
這只是個人學習多線程相關知識而簡單實現的日志類,功能簡單也未做任何優化。如果需要在實際項目中使用日志記錄功能,推薦開源的Nlog