日常在多線程開發中,總避免不了線程同步。本篇對net多線程中的鎖系統作個簡單描述。html
Lock是Monitor語法糖簡化寫法,Lock在IL會生成Monitor。c#
//======Example 1===== string obj = "helloworld"; lock (obj) { Console.WriteLine(obj); } //lock IL會編譯成以下寫法 bool isGetLock = false; Monitor.Enter(obj, ref isGetLock); try { Console.WriteLine(obj); } finally { if (isGetLock) { Monitor.Exit(obj); } }
isGetLock參數是Framework 4.0後新加的。 爲了使程序在全部狀況下都可以肯定,是否有必要釋放鎖。例: Monitor.Enter拿不到鎖windows
Monitor.Enter 是能夠鎖值類型的。鎖時會裝箱成新對象,因此沒法作到線程同步。數據結構
一:Lock是隻能在進程內鎖,不能跨進程,內部走的是混合構造,先自旋再轉成內核構造。多線程
二:關於對type類型的鎖,以下:app
//======Example 2===== new Thread(new ThreadStart(() => { lock (typeof(int)) { Thread.Sleep(10000); Console.WriteLine("Thread1釋放"); } })).Start(); Thread.Sleep(1000); lock(typeof(int)) { Console.WriteLine("Thread2釋放"); }
運行結果以下:函數
在看個例子:性能
//======Example 3===== Console.WriteLine(DateTime.Now); AppDomain appDomain1 = AppDomain.CreateDomain("AppDomain1"); LockTest Worker1 = (LockTest)appDomain1.CreateInstanceAndUnwrap( Assembly.GetExecutingAssembly().FullName, "ConsoleApplication1.LockTest"); Worker1.Run(); AppDomain appDomain2 = AppDomain.CreateDomain("AppDomain2"); LockTest Worker2 = (LockTest)appDomain2.CreateInstanceAndUnwrap( Assembly.GetExecutingAssembly().FullName, "ConsoleApplication1.LockTest"); Worker2.Run(); /// <summary> /// 跨應用程序域邊界或遠程訪問時須要繼承MarshalByRefObject /// </summary> public class LockTest : MarshalByRefObject { public void Run() { lock (typeof(int)) { Thread.Sleep(10000); Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + ": Thread 釋放," + DateTime.Now); } } }
運行結果以下:測試
第一個例子說明,在同進程同域,不一樣線程下,鎖type int,其實鎖的是同一個int對象,因此要慎用。ui
第二個例子,這裏就簡單說下。
A: CLR啓動時,會建立 系統域(System Domain)和共享域(Shared Domain), 默認程序域(Default AppDomain)。 系統域和共享域是單例的。程序域能夠有多個,例子中咱們使用AppDomain.CreateDomain方法建立的。
B: 按正常來講,每一個程序域的代碼都是隔離,互不影響的。但對於一些基礎類型來講,每一個程序域都從新加載一份,就顯得有點浪費,帶來額外的損耗壓力。聰明的CLR會把一些基本類型Object, ValueType, Array, Enum, String, and Delegate等所在的程序集MSCorLib.dll,在CLR啓動過程當中都會加載到共享域。 每一個程序域都會使用共享域的基礎類型實例。
C: 而每一個程序域都有屬於本身的託管堆。託管堆中最重要的是GC heap和Loader heap。GC heap用於引用類型實例的存儲,生命週期管理和垃圾回收。Loader heap保存類型系統,如MethodTable,數據結構等,Loader heap生命週期不受GC管理,跟程序域卸載有關。
因此共享域中Loader heap MSCorLib.dll中的int實例會一直保留着,直到進程結束。單個程序域卸載也不受影響。做用域很大有沒有!!!
這時第二個例子也很容易理解了。 鎖int實例是跨程序域的,MSCorLib中的基礎類型都是這樣, 極容易形成死鎖。 而自定義類型則會加載到本身的程序域,不會影響其餘。
咱們都知道鎖的目的,是爲了多線程下值被破壞。也知道string在c#是個特殊對象,值是不變的,每次變更都是一個新對象值,這也是推薦stringbuilder緣由。如例:
//======Example 4===== string str1 = "mushroom"; string str2 = "mushroom"; var result1 = object.ReferenceEquals(str1, str2); var result2 = object.ReferenceEquals(str1, "mushroom"); Console.WriteLine(result1 + "-" + result2); /* output * True-True */
正是因爲c#中字符串的這種特性,因此字符串是在多線程下是不會被修改的,只讀的。它存在於SystemDomain域中managed heap中的一個hash table中。其中Key爲string自己,Value爲string對象的地址。
當程序域須要一個string的時候,CLR首先在這個Hashtable根據這個string的hash code試着找對應的Item。若是成功找到,則直接把對應的引用返回,不然就在SystemDomain對應的managed heap中建立該 string,並加入到hash table中,並把引用返回。因此說字符串的生命週期是基於整個進程的,也是跨AppDomain。
簡單介紹下Wait,Pulse,PulseAll的用法,已加註釋。
static string str = "mushroom"; static void Main(string[] args) { new Thread(() => { bool isGetLock = false; Monitor.Enter(str, ref isGetLock); try { Console.WriteLine("Thread1第一次獲取鎖"); Thread.Sleep(5000); Console.WriteLine("Thread1暫時釋放鎖,並等待其餘線程釋放通知信號。"); Monitor.Wait(str); Console.WriteLine("Thread1接到通知,第二次獲取鎖。"); Thread.Sleep(1000); } finally { if (isGetLock) { Monitor.Exit(str); Console.WriteLine("Thread1釋放鎖"); } } }).Start(); Thread.Sleep(1000); new Thread(() => { bool isGetLock = false; Monitor.Enter(str, ref isGetLock); //一直等待中,直到其餘釋放。 try { Console.WriteLine("Thread2得到鎖"); Thread.Sleep(5000); Monitor.Pulse(str); //通知隊列裏一個線程,改變鎖狀態。 Pulseall 通知全部的 Console.WriteLine("Thread2通知其餘線程,改變狀態。"); Thread.Sleep(1000); } finally { if (isGetLock) { Monitor.Exit(str); Console.WriteLine("Thread2釋放鎖"); } } }).Start(); Console.ReadLine();
lock是不能跨進程鎖的。 mutex做用和lock相似,可是它能跨進程鎖資源(走的是windows內核構造),如例子:
static bool createNew = false; //第一個參數 是否應擁有互斥體的初始所屬權。即createNew true時,mutex默認得到處理信號 //第二個是名字,第三個是否成功。 public static Mutex mutex = new Mutex(true, "mushroom.mutex", out createNew); static void Main(string[] args) { //======Example 5===== if (createNew) //第一個建立成功,這時候已經拿到鎖了。 無需再WaitOne了。必定要注意。 { try { Run(); } finally { mutex.ReleaseMutex(); //釋放當前鎖。 } } //WaitOne 函數做用是阻止當前線程,直到拿到收到其餘實例釋放的處理信號。 //第一個參數是等待超時時間,第二個是否退出上下文同步域。 else if (mutex.WaitOne(10000,false))// { try { Run(); } finally { mutex.ReleaseMutex(); } } else//若是沒有發現處理信號 { Console.WriteLine("已經有實例了。"); Console.ReadLine(); } } static void Run() { Console.WriteLine("實例1"); Console.ReadLine(); }
順序啓動A B實例測試下。A首先拿到鎖,輸出 實例1 。B在等待, 若是10秒內A釋放,B拿到執行Run()。超時後輸出"已經有實例了"。
這裏注意的是第一個拿處處理信號 的實例,已經拿到鎖了。不須要再WaitOne。 不然報異常。
即信號量,咱們能夠把它理解爲升級版的mutex。mutex對一個資源進行鎖,semaphore則是對多個資源進行加鎖。
semaphore是由windows內核維持一個int32變量的線程計數器,線程每調用一次、計數器減1、釋放後對應加一, 超出的線程則排隊等候。
走的是內核構造,因此semaphore也是能夠跨進程的。
static void Main(string[] args) { Console.WriteLine("準備處理隊列"); bool createNew = false; SemaphoreSecurity ss = new SemaphoreSecurity(); //信號量權限控制 Semaphore semaphore = new Semaphore(2, 2, "mushroom.Semaphore", out createNew,null); for (int i = 1; i <= 5; i++) { new Thread((arg) => { semaphore.WaitOne(); Console.WriteLine(arg + "處理中"); Thread.Sleep(10000); semaphore.Release(); //即semaphore.Release(1) //semaphore.Release(5);能夠釋放多個,但不能超過最大值。若是最後釋放的總量超過自己總量,也會報錯。 不建議使用 }).Start(i); } Console.ReadLine(); }
mutex、Semaphore 須要先把託管代碼轉成本地用戶模式代碼、再轉換成本地內核代碼。
當釋放後須要從新轉換成託管代碼,性能會有必定的損耗,因此儘可能在須要跨進程的場景再使用。
參考 http://www.cnblogs.com/artech/archive/2007/06/04/769805.html