本文主要是實現作業系統層級的多進程間線程同步(進程同步)的範例程式碼及測試結果。代碼經過測試,可供參考,也可直接使用。
承接上一篇部落格的業務情境[C#使用讀寫鎖三行代碼簡單解決多線程並發寫入檔案時線程同步的問題]。
隨著服務進程的增多,光憑進程內的線程同步已經不能滿足現在的需求,導致多進程同時寫入同一個檔案時,一樣提示檔案被佔用的問題。
在這種情境下,跨進程級的鎖是不可避免的。在.NET提供的參考中,進程鎖都繼承了System.Threading.WaitHandle類。
而在本文中針對單個檔案同一時間僅允許單個進程(線程)操作的情境,System.Threading.Mutex類無疑是最簡單也是最合適的選擇。
該類型的對象可以使用命名(字串)互斥量實現當前會話級或作業系統級的同步需求。我選擇了作業系統層級的同步編寫樣本,因為覆蓋面更廣。
下面是實現代碼,注釋很詳細就不細說了:
namespace WaitHandleExample{ class Program { static void Main(string[] args) { #region 簡單使用 //var mutexKey = MutexExample.GetFilePathMutexKey("檔案路徑"); //MutexExample.MutexExec(mutexKey, () => //{ // Console.WriteLine("需要進程同步執行的代碼"); //}); #endregion #region 測試代碼 var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test.log").ToUpper(); var mutexKey = MutexExample.GetFilePathMutexKey(filePath); //同時開啟N個寫入線程 Parallel.For(0, LogCount, e => { //沒使用互斥鎖操作寫入,大量寫入錯誤;FileStream包含FileShare的建構函式也僅實現了進程內的線程同步,多進程同時寫入時也會出錯 //WriteLog(filePath); //使用互斥鎖操作寫入,由於同一時間僅有一個線程操作,所以不會出錯 MutexExample.MutexExec(mutexKey, () => { WriteLog(filePath); }); }); Console.WriteLine(string.Format("Log Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString())); Console.Read(); #endregion } /// <summary> /// C#互斥量使用範例程式碼 /// </summary> /// <remarks>已在經過測試並上線運行,可直接使用</remarks> public static class MutexExample { /// <summary> /// 進程間同步執行的簡單例子 /// </summary> /// <param name="action">同步處理代碼</param> /// <param name="mutexKey">作業系統級的同步鍵 /// (如果將 name 指定為 null 或Null 字元串,則建立一個局部互斥體。 /// 如果名稱以首碼“Global\”開頭,則 mutex 在所有終端伺服器會話中均為可見。 /// 如果名稱以首碼“Local\”開頭,則 mutex 僅在建立它的終端伺服器會話中可見。 /// 如果建立已命名 mutex 時不指定首碼,則它將採用首碼“Local\”。)</param> /// <remarks>不重試且不考慮異常情況處理的簡單例子</remarks> [Obsolete(error: false, message: "請使用MutexExec")] public static void MutexExecEasy(string mutexKey, Action action) { //聲明一個已命名的互斥體,實現進程間同步;該命名互斥體不存在則自動建立,已存在則直接擷取 using (Mutex mut = new Mutex(false, mutexKey)) { try { //上鎖,其他線程需等待釋放鎖之後才能執行處理;若其他線程已經上鎖或優先上鎖,則先等待其他線程執行完畢 mut.WaitOne(); //執行處理代碼(在調用WaitHandle.WaitOne至WaitHandle.ReleaseMutex的時間段裡,只有一個線程處理,其他線程都得等待釋放鎖後才能執行該程式碼片段) action(); } finally { //釋放鎖,讓其他進程(或線程)得以繼續執行 mut.ReleaseMutex(); } } } /// <summary> /// 擷取檔案名稱對應的進程同步鍵 /// </summary> /// <param name="filePath">檔案路徑(請注意大小寫及空格)</param> /// <returns>進程同步鍵(互斥體名稱)</returns> public static string GetFilePathMutexKey(string filePath) { //組建檔案對應的同步鍵,可自訂格式(互斥體名稱對特殊字元支援不友好,遂轉換為BASE64格式字串) var fileKey = Convert.ToBase64String(Encoding.Default.GetBytes(string.Format(@"FILE\{0}", filePath))); //轉換為作業系統級的同步鍵 var mutexKey = string.Format(@"Global\{0}", fileKey); return mutexKey; } /// <summary> /// 進程間同步執行 /// </summary> /// <param name="mutexKey">作業系統級的同步鍵 /// (如果將 name 指定為 null 或Null 字元串,則建立一個局部互斥體。 /// 如果名稱以首碼“Global\”開頭,則 mutex 在所有終端伺服器會話中均為可見。 /// 如果名稱以首碼“Local\”開頭,則 mutex 僅在建立它的終端伺服器會話中可見。 /// 如果建立已命名 mutex 時不指定首碼,則它將採用首碼“Local\”。)</param> /// <param name="action">同步處理操作</param> public static void MutexExec(string mutexKey, Action action) { MutexExec(mutexKey: mutexKey, action: action, recursive: false); } /// <summary> /// 進程間同步執行 /// </summary> /// <param name="mutexKey">作業系統級的同步鍵 /// (如果將 name 指定為 null 或Null 字元串,則建立一個局部互斥體。 /// 如果名稱以首碼“Global\”開頭,則 mutex 在所有終端伺服器會話中均為可見。 /// 如果名稱以首碼“Local\”開頭,則 mutex 僅在建立它的終端伺服器會話中可見。 /// 如果建立已命名 mutex 時不指定首碼,則它將採用首碼“Local\”。)</param> /// <param name="action">同步處理操作</param> /// <param name="recursive">指示當前調用是否為遞迴處理,遞迴處理時檢測到異常則拋出異常,避免進入無限遞迴</param> private static void MutexExec(string mutexKey, Action action, bool recursive) { //聲明一個已命名的互斥體,實現進程間同步;該命名互斥體不存在則自動建立,已存在則直接擷取 //initiallyOwned: false:預設當前線程並不擁有已存在互斥體的所屬權,即預設本線程並非為首次建立該命名互斥體的線程 //注意:並發聲明同名的命名互斥體時,若間隔時間過短,則可能同時聲明了多個名稱相同的互斥體,並且同名的多個互斥體之間並不同步,高並發使用者請另行處理 using (Mutex mut = new Mutex(initiallyOwned: false, name: mutexKey)) { try { //上鎖,其他線程需等待釋放鎖之後才能執行處理;若其他線程已經上鎖或優先上鎖,則先等待其他線程執行完畢 mut.WaitOne(); //執行處理代碼(在調用WaitHandle.WaitOne至WaitHandle.ReleaseMutex的時間段裡,只有一個線程處理,其他線程都得等待釋放鎖後才能執行該程式碼片段) action(); } //當其他進程已上鎖且沒有正常釋放互斥鎖時(譬如進程忽然關閉或退出),則會拋出AbandonedMutexException異常 catch (AbandonedMutexException ex) { //避免進入無限遞迴 if (recursive) throw ex; //非遞迴調用,由其他進程拋出互斥鎖解鎖異常時,重試執行 MutexExec(mutexKey: mutexKey, action: action, recursive: true); } finally {