寫在前面:
在多線程編程中,可能會有許多線程並發的執行一段代碼。在某些情況下,我們希望A中的代碼塊(B)同步的執行,即同一時刻只有一個線程執行代碼塊B,這就需要用到鎖(lock)。lock 關鍵字可以用來確保代碼塊完成運行,而不會被其他線程中斷。它可以把一段代碼定義為互斥段(critical section),互斥段在一個時刻內只允許一個線程進入執行,而其他線程必須等待,以達到安全訪問。
舉一個例子:現有十個蘋果,張三和李四同時吃這些蘋果
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace LockTest { class Program { private static int apple = 10;//10個蘋果 static void Main(string[] args) { Thread t1 = new Thread(() => EatApple("張三")); Thread t2 = new Thread(() => EatApple("李四")); t1.IsBackground = true; t2.IsBackground = true; t1.Start(); t2.Start(); Console.ReadKey(); } private static void EatApple(string name) { while (true) { apple -= 1; Console.WriteLine(name + "正在吃蘋果"); Thread.Sleep(3000); Console.WriteLine(name + "吃完了,還剩" + apple + "個蘋果\n"); if (apple <= 0) break; } } } }
結果是這樣的混亂:
然后我們把共同訪問的代碼加上鎖之后:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace LockTest { class Program { private static int apple = 10; //10個蘋果 private static object locker = new object();//創建鎖 static void Main(string[] args) { Thread t1 = new Thread(() => EatApple("張三")); Thread t2 = new Thread(() => EatApple("李四")); t1.IsBackground = true; t2.IsBackground = true; t1.Start(); t2.Start(); Console.ReadKey(); } private static void EatApple(string name) { while (true) { lock (locker)//加鎖 { apple -= 1; Console.WriteLine(name + "正在吃蘋果"); Thread.Sleep(3000); Console.WriteLine(name + "吃完了,還剩" + apple + "個蘋果\n"); if (apple <= 1)//變為1 不然會吃-1個蘋果 break; } } } } }
結果如下,加上鎖之后呢,兩個人就只能一個人一個人的去拿蘋果吃。
%注%:
lock(this) 鎖定 當前實例對象,如果有多個類實例的話,lock鎖定的只是當前類實例,對其它類實例無影響。 lock(typeof(Model))鎖定的是model類的所有實例。 lock(obj)鎖定的對象是全局的私有化靜態變量。外部無法對該變量進行訪問。 lock 確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。 所以,lock的結果好不好,還是關鍵看鎖的誰,如果外邊能對這個誰進行修改,lock就失去了作用。所以一般情況下,使用私有的、靜態的並且是只讀的對象。
1、lock的是必須是引用類型的對象,string類型除外。
2、lock推薦的做法是使用靜態的、只讀的、私有的對象。
3、保證lock的對象在外部無法修改才有意義,如果lock的對象在外部改變了,對其他線程就會暢通無阻,失去了lock的意義。
不能鎖定字符串,鎖定字符串尤其危險,因為字符串被公共語言運行庫 (CLR)“暫留”。 這意味着整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應用程序域的所有線程中的該文本。因此,只要在應用程序進程中的任何位置處具有相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的所有實例。通常,最好避免鎖定 public 類型或鎖定不受應用程序控制的對象實例。例如,如果該實例可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的代碼也可能會鎖定該對象。這可能導致死鎖,即兩個或更多個線程等待釋放同一對象。出於同樣的原因,鎖定公共數據類型(相比於對象)也可能導致問題。而且lock(this)只對當前對象有效,如果多個對象之間就達不到同步的效果。lock(typeof(Class))與鎖定字符串一樣,范圍太廣了。
lock用法:
其寫法如下: Object locker = new Object(); lock(locker) { //此處放置同步執行的代碼 }
相當於:
private static object locker = new object();//創建鎖 Monitor.Enter(locker); //排它鎖 { //此處放置同步執行的代碼 } Monitor.Exit(locker); //釋放指定對象上的排他鎖
Monitor的常用屬性和方法:
Enter(Object) 在指定對象上獲取排他鎖。
Exit(Object) 釋放指定對象上的排他鎖。
Pulse 通知等待隊列中的線程鎖定對象狀態的更改。
PulseAll 通知所有的等待線程對象狀態的更改。
TryEnter(Object) 試圖獲取指定對象的排他鎖。
TryEnter(Object, Boolean) 嘗試獲取指定對象上的排他鎖,並自動設置一個值,指示是否得到了該鎖。
Wait(Object) 釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖。
常用的方法有兩個,Monitor.Enter(object)方法是獲取鎖,Monitor.Exit(object)方法是釋放鎖,這就是Monitor最常用的兩個方法,在使用過程中為了避免獲取鎖之后因為異常,致鎖無法釋放,所以需要在try{} catch(){}之后的finally{}結構體中釋放鎖(Monitor.Exit())。
TryEnter(Object)和TryEnter() 方法在嘗試獲取一個對象上的顯式鎖方面和 Enter() 方法類似。然而,它不像Enter()方法那樣會阻塞執行。如果線程成功進入關鍵區域那么TryEnter()方法會返回true. 和試圖獲取指定對象的排他鎖。
我們可以通過Monitor.TryEnter(monster, 1000),該方法也能夠避免死鎖的發生,Monitor.TryEnter(Object,Int32)。
設置1S的超時時間,如果在1S之內沒有獲得同步鎖,則返回false,也就是說,在1秒中后,lockObj還未被解鎖,TryEntry方法就會返回false,如果在1秒之內,lockObj被解鎖,TryEntry返回true。我們可以使用這種方法來避免死鎖
Monitor.Wait和Monitor()Pause()
Wait(object)方法:釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖,該線程進入等待隊列。
Pulse方法:只有鎖的當前所有者可以使用 Pulse 向等待對象發出信號,當前擁有指定對象上的鎖的線程調用此方法以便向隊列中的下一個線程發出鎖的信號。接收到脈沖后,等待線程就被移動到就緒隊列中。在調用 Pulse 的線程釋放鎖后,就緒隊列中的下一個線程(不一定是接收到脈沖的線程)將獲得該鎖。
另外:Wait 和 Pulse 方法必須寫在 Monitor.Enter 和Moniter.Exit 之間。