1.lock的本質
實現線程同步的第一種方式是我們經常使用的lock關鍵字,它將包圍的語句塊標記為臨界區,這樣一次只有一個線程進入臨界區並執行代碼。下面第一段的幾行代碼是關於lock關鍵字的使用方式,但更重要的是我們可以通過這個例子來看到lock關鍵字的本質。第二段是這個方法的IL指令集,從中可以看到lock其實也是一個語法糖,它的內部實現是采用了監視器Monitor。第三段代碼是我寫的lock內部實現的C#代碼,由於lock內部有finally關鍵字,這將保證內部最後一定會執行exit方法。而如果我們自己使用Monitor的話有可能會一不小心忘記調用exit方法,因此使用lock更加可靠。在使用lock關鍵字時必須使用一個引用類型的參數,我如果將i字段放入lock會提示報錯,可見使用lock無法將i進行裝箱。當然這只是猜測,本質的原因將在後面解釋。在上例中我new了一個o對象,這樣鎖的范圍只包括lock語句塊。如果lock中傳進來的參數是一個外部對象,那麼鎖的范圍將擴展到這個對象。官方文檔上有這樣一句話:“嚴格來說,提供的對象是用來唯一地標識由多個線程共享的資源,所以它可以是任意類型。然而實際上,此對象通常表示需要進行線程同步的資源“。從這句話可以知道選擇引用參數時,應該選擇多個線程需要操作的共享對象,像我這裡隨便創建的object對象不是官方推薦的做法。
public void MyLock() { int i = 0; object o=new object(); lock (o) { i = 5; } }View Code
.method public hidebysig instance void MyLock() cil managed { .maxstack 2 .locals init ( [0] int32 num, [1] object obj2, [2] bool flag, [3] object obj3, [4] bool flag2) L_0000: nop L_0001: ldc.i4.0 L_0002: stloc.0 L_0003: newobj instance void [mscorlib]System.Object::.ctor() L_0008: stloc.1 L_0009: ldc.i4.0 L_000a: stloc.2 L_000b: ldloc.1 L_000c: dup L_000d: stloc.3 L_000e: ldloca.s flag L_0010: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) L_0015: nop L_0016: nop L_0017: ldc.i4.5 L_0018: stloc.0 L_0019: nop L_001a: leave.s L_002e L_001c: ldloc.2 L_001d: ldc.i4.0 L_001e: ceq L_0020: stloc.s flag2 L_0022: ldloc.s flag2 L_0024: brtrue.s L_002d L_0026: ldloc.3 L_0027: call void [mscorlib]System.Threading.Monitor::Exit(object) L_002c: nop L_002d: endfinally L_002e: nop L_002f: ret .try L_000b to L_001c finally handler L_001c to L_002e }View Code
public void MyLock2() { int i = 0; //注意isOk必須設置為false。如果加鎖成功則會將isOk設置為true,否則為false。 //在加鎖過程中如果沒有異常,那麼會將isOk設置為true。 bool isOk=false; object o = new object(); Monitor.Enter(o, ref isOk); try { i = 5; } finally { Monitor.Exit(o); } }View Code
2.lock的參數
在給lock傳遞參數時首先要避免使用public對象,因為有可能外部程序也在對這個對象加鎖,比如下面第一段代碼。可以看到有可能出現一種情況,那就是主線程執行到lock(objA)時,正好線程t執行到lock(objB)。此時objA被t鎖住,objB又被主線程鎖住,死鎖就這樣發生了。其次,如果使用public對象,那麼程序員可能會使用lock(this)、lock(typeof(myType))、lock("string")。而上述三種情況微軟都是不建議我們使用的,對於後兩種情況msdn已經解釋的很清楚這裡我就不寫了。可能是一開始看了網上一些錯誤的帖子,讓我困擾了一段時間的是lock(this)。msdn原話是”lock(this) 可能會有問題,因為不受控制的代碼也可能會鎖定該對象。這可能導致死鎖“。我蠻想知道得到這樣一種情況,使用lock(this)會發送死鎖,而不使用lock(this)則不會發生死鎖。假設要發生死鎖,那麼有2個多個線程將互相等待。現在我要使用lock(this)來讓死鎖發生,所謂this就是鎖定了當前執行方法的實例對象,而這個實例對象是public的。因此這種情況和上面使用public對象發生死鎖的本質是一樣的,那就是外部線程也要對該實例對象加鎖,從而造成了2個線程相互等待的情況。比如下面第二段代碼,結果和第一段代碼類似(如下圖),只不過在代碼中使用了this關鍵字,同樣也發生了死鎖。
class Program { static void Main(string[] args) { MyClass myClass = new MyClass(); Thread t = new Thread(myClass.LockFunc); t.Start(); lock (myClass.objB) { Console.WriteLine("我是主線程,已對objB加鎖,馬上加鎖objA"); /* 結果是:我是主線程,已對objB加鎖,馬上加鎖objA 我是線程t,已對objA加鎖,馬上加鎖objB 可見此時發送了死鎖,這是一種情況,多試幾次會出現順利執行的結果。 */ lock (myClass.objA) { Console.WriteLine("我是主線程,已對objA、objB都加鎖"); } } } } class MyClass { public object objA = new object(); public object objB = new object(); public void LockFunc() { lock (objA) { Console.WriteLine("我是線程t,已對objA加鎖,馬上加鎖objB"); lock (objB) { Console.WriteLine("我是線程t,已對objA、objB都加鎖"); } } } }View Code
class Program { static void Main(string[] args) { object obj=new object(); MyClass myClass = new MyClass(); Thread t = new Thread(myClass.LockFunc); t.Start(obj); lock (myClass) { Console.WriteLine("我是主線程,已對myClass加鎖,馬上加鎖obj"); lock (obj) { Console.WriteLine("我是主線程,已對obj、myClass都加鎖"); } } } } class MyClass { public void LockFunc(object obj) { lock (obj) { Console.WriteLine("我是線程t,已對obj加鎖,馬上加鎖myClass"); lock (this) { Console.WriteLine("我是線程t,已對obj、myClass都加鎖"); } } } }View Code
現在如果要設置lock參數,我們知道要首選私有成員。不過除了將對象設置為私有成員外,我們還應該最好將其設置為只讀成員。如果沒有將對象設置為只讀成員,那這種情況一定要注意,因為它會讓鎖失效。代碼如下所示, 這段代碼的運行結果是下面的第一張圖。從圖中可以看到因為使用了lock加鎖因此count永遠不會大於100且一定連續的先後出現"加20前"、"加20後"。現在將代碼中的objA=objB取消注釋再執行,得到的結果如第二張圖所示,可以發現現在並沒有線程同步了。第一句是"線程1執行中count加20前",如果線程同步那麼接下來一定是"線程1執行中count加20後",可是結果卻是線程2在執行,這說明鎖失效了。可以看到表面原因是改變了objA指向的對象,從而導致鎖失效。但是再往深處想,我很好奇加鎖到底在對象上做了什麼?雖然改變了objA指向的對象,但確實不是有一個對象已經被加鎖了嗎?
在學習了同步塊索引後,這些問題的答案也就出現了。我們知道每一個對象內存中都有一個同步塊索引和類型指針,當在堆上創建一個對象時它的同步塊索引會被設置為一個負數,這表明現在沒有線程對它加鎖。在CLR初始化時它會分配一個同步塊數組,這個數組的大小是可以動態改變的且不在GC中。當使用lock對一個obj加鎖時,將會讓obj內存中的同步塊索引與CLR中的同步塊數組中的某一項關聯起來,也就是讓同步塊索引可以索引到同步塊數組中的這一項。當不再有需要對象同步的線程時,這個對象的同步塊索引將會被再次重置為負數,同步塊數組的這一項將可以繼續與對象進行關聯。而線程執行時如果發現要鎖定的對象中的同步塊索引已經指向了同步塊數組,那麼該線程將會進入等待隊列。再來看這個例子,在對objA加鎖後,objA的同步塊索引已經關聯同步塊數組。執行objA=objB後,objA執行了堆中的objB堆空間,而objB的同步塊索引是沒有加鎖的,因此線程2執行到lock(objA)時其實是對objB加了鎖。所以線程1執行的過程中線程2也進入到了我們希望的同步區,結果中顯示的是線程2進入同步區後一口氣執行執行完畢。如果線程2只執行了一部分,此時線程1遇到了lock(objA),那麼此時它會等待線程2執行完畢,因此結果輸出中線程2都是一口氣執行完畢。當然在多次運行程序過程中還有不同的結果,一是線程1執行完畢後線程2才進入;二是線程1執行到lock(objA)時正好線程2已經釋放對objB的加鎖,這樣線程1就可以順利的對objA(實際上是objB)加鎖了,那麼此時線程2就要等待線程1了。
class Program { static void Main(string[] args) { MyLock myLock=new MyLock(); Thread thread1 = new Thread(myLock.Thread1Func); thread1.Start(); Thread thread2 = new Thread(myLock.Thread2Func); thread2.Start(); } } class MyLock { int count = 0; object objA = new object(); object objB = new object(); public void Thread1Func() { for (int i = 0; i < 5; i++) StartEatPear("線程1"); } public void Thread2Func() { for (int i = 0; i < 5; i++) StartEatPear("線程2"); } public int StartEatPear(string str) { lock (objA) { //在臨界區中修改objA會導致鎖失效 //objA = objB; if (count <100) { Console.WriteLine(str+"執行中,count加20前:" + count); count = count + 20; Console.WriteLine(str + "執行中,count加20後:" + count); } else { Console.WriteLine(str + "執行中,count將return:" + count); return count; } } return count; } }View Code
3.還是lock(this)
在寫使用this發生死鎖時,我寫著好玩用了雙this,代碼如下。剛開始我很好奇線程執行到第二個this時會不會在這裡等待,不過我覺得既然是同一個線程執行,內部應該會采取措施來讓線程繼續執行。最後結果是輸出了2條語句,線程執行到第二個this時沒有等待而是直接執行。查閱資料知道加鎖的流程後這個問題才解決。我們知道lock其實是調用了靜態方法Enter,這個方法首先會做判斷,如果此時這個對象沒有被鎖住且沒有線程在等待,那麼這個對象將與同步塊相關聯以達到同步的效果。否則,也就是說這個對象已被鎖住了,接下來有2種情況。一種情況是當前線程正好是將對象加鎖的線程,那麼此時會設置一個字段加1;另一種情況是當前線程不是將該對象加鎖的線程,因此當前線程只能進入等待隊列了。這樣只要是同樣的線程,即使遇到多個相同的lock語句將會接續執行而不會等待。
在使用lock(this)時,由於已對這個對象加鎖,因此其他線程無法再對這個對象加鎖,現在加鎖的本質也清楚了,可是這似乎只是在調用Monitor.Enter()方法時進行的。加了鎖之後對於這個對象我們只知道無法再次加鎖,但是到底還可不可以訪問呢?如下面第二段代碼,我創建的線程t1在無限循環的執行臨界區,除非isgo被設置為false否則將不會有線程可以對myClass對象加鎖。接著我在主線程調用NotLockMe方法,程序執行結果如代碼中所示,主線程依舊可以訪問myClass對象。這說明加鎖並不是像我原先理解的那樣會完全將這個對象鎖住而其他地方無法訪問這個對象,加鎖僅僅只是讓其他線程不可以對已加鎖的對象再進行加鎖,這個概念要理解清楚。順其自然的對於寫加鎖代碼我有一個想法,是不是可以設置一個bool值,如果在預期時間內沒有出現想要的結果,我們可以通過在外部設置這個bool值讓臨界區退出執行。小弟新手一枚,代碼寫的還不夠多,學的還不夠深入,如有錯誤還請各位前輩指出!
class Program { static void Main(string[] args) { MyClass myClass = new MyClass(); Thread t = new Thread(myClass.LockFunc); t.Start(); } } class MyClass { public void LockFunc() { lock (this) { Console.WriteLine("我是第一個this"); lock (this) { Console.WriteLine("我是第二個this"); } } } }View Code
class Program { static void Main(string[] args) { MyClass myClass = new MyClass(); Thread t1 = new Thread(myClass.LockMe); t1.Start(); Thread.Sleep(100); //調用沒有被lock的方法 myClass.NotLockMe(); //結果是: // I am locked // I am not locked } } class MyClass { private bool isgo = true; public void LockMe() { lock (this) { while (isgo) { Console.WriteLine("I am locked"); Thread.Sleep(500); } } } //所有線程都可以同時訪問的方法 public void NotLockMe() { isgo = false; Console.WriteLine("I am not locked"); } }View Code