先考慮一個問題:服務端接受多個客戶端提交的視頻文件進行轉碼的操作,應該怎么設計?
由於轉碼比較花費時間,所以我們排除同步的想法。而轉碼需要用到的外部軟件(exe文件),不能同時被多個線程用到,所以我們排除為每一個客戶端提交新建一個線程進行轉碼的想法。
於是我們想到了靜態加鎖和隊列。靜態加鎖有個缺點,稍后再提。當我們選擇了隊列,就選擇了生產者消費者模式。
其流程圖:
有流程圖我們可以知道,生產者不關心數據什么時候被處理,消費者不關心數據什么時候產生,實現了解耦,也解決了阻塞。
還有一個比較典型的例子便是日志的記錄,多線程產生日志,但寫日志由於文件獨占,不能多線程來寫,於是我們就可以把線程壓入隊列,由日志線程來讀取隊列數據,完成寫日志的操作。下面是一個簡單的實現:
public class Log { private static ConcurrentQueue<LogMessage> msgs = new ConcurrentQueue<LogMessage>(); public static void WriteLog(string msg) { msgs.Enqueue(new LogMessage(msg)); } public static void Start() { Task.Factory.StartNew(() => { while (true) { while (msgs.TryDequeue(out LogMessage msg)) { using (StreamWriter sw = new StreamWriter(msg.LogFile, true)) { sw.WriteLine(msg.Message); } } Thread.Sleep(1000); } }); } }
這個是寫日志的類
class LogMessage { public string Message { get; set; } public string LogFile { get; set; } public LogMessage(string msg) { this.Message = $"{DateTime.Now.ToString("HH:mm:ss ")} {msg}"; string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log", DateTime.Now.ToString("yyyy-MM")); if (!Directory.Exists(path)) Directory.CreateDirectory(path); this.LogFile = Path.Combine(path, DateTime.Now.ToString("dd") + ".log"); } }
這個是日志結構。包括產生日志的時間和寫日志的日志文件。可以實現23:59產生的日志寫到當天的文件夾中。
日志工具類的調用也非常簡單,直接調用靜態方法WriteLog就行。
回到開頭所說加鎖的弊端:線程排隊並不是在隊列中,沒有先后順序的保證,牽扯到嚴格順序時就會有問題,比如寫日志,socket數據接受等。
模式的應用場景:處理數據比較消耗時間,線程獨占,生產數據不需要即時的反饋等。
例子的不足:
1.省略掉了緩沖區,使得生產者和消費者並不是完全解綁。改進:用一個獨立的數據結構來放置數據,可以是緩存、文件、數據庫,實現僅依賴於數據格式的解綁。
2.程序結束時,我們不能保證緩沖區數據是否全部處理完。改進:生產日志時,寫文件/數據庫,處理數據后,對處理過的數據進行標記,程序異常結束也沒問題,下次重啟先加載未處理數據,再一次展現單純加鎖的弊端。