以下內容針對互斥鎖。
為什么需要鎖?
鎖代表着對臨界區的訪問權限。只有獲得鎖的操作對象,才能進入臨界區。
鎖的本質是什么?
鎖的本質是一個數據結構(或者說是一個對象),這個對象內保留着描述鎖所需要的必要信息。如當前鎖是否已被占用,被哪個線程占用。而鎖的一些工具,函數庫,實際上就是對一個鎖對象的信息進行變更。
上鎖操作 => 嘗試對鎖對象的信息進行修改,如果修改成功,則程序繼續向下執行,否則將暫時停留在此。(停留的方式有兩種,一種是自旋反復嘗試,另一種是掛起等待喚醒)
解鎖操作 => 重置鎖對象的信息。
類似下面這樣(注:這個例子不准確,后面會講)
typedef struct __lock_t { int flag; //鎖的狀態 0-空閑, 1-被占用 } lock_t; void init(lock_t *mutex) { //初始化鎖對象 mutex->flag = 0; } void lock(lock_t *mutex) { while(mutex->flag == 1) ;// 自旋等待 mutex->flag = 1; } void unlock(lock_t *mutex) { mutex->flag = 0; }
鎖信息的存儲位置
一種是保留在進程內,由於操作系統提供的內存虛擬化,所以這個鎖對象的內存空間,只能被當前進程訪問。並且同一進程的線程可以共享內存資源。所以,這個鎖對象只能被當前進程的線程所訪問。
另一種是將鎖的信息保存在本機的其他應用中。例如本機沒有開啟外部訪問的Redis。這樣本機的多個應用就可以通過Redis中的這個鎖的信息進行調度管理。
還有一種就是將鎖的信息保存在其他機器中(或者本機開啟外部訪問的Redis中),這樣其他電腦的應用也可以對這個鎖進行訪問,這就是分布式鎖。
對鎖信息進行修改
存在的問題
前面有提到,前面的lock函數對鎖信息的修改操作存在問題,我們來看看問題到底出在哪里。假設,我們的電腦只有一個CPU,這個時候有兩個線程開始嘗試獲取鎖。
這個程序的結果是,在線程B已經占用鎖的時候,線程A還能獲取到鎖。這就不能滿足"互斥鎖"的定義,這段代碼就不滿足正確性。那么問題出在哪里呢?問題就在於判斷和修改這兩個操作沒有原子性。
正如上面的例子那樣,線程A剛執行完判斷,還沒來得及做修改操作,就發生了上下文切換,轉而執行線程B的代碼。切換回線程A的時候,實際上條件已經發生了變更。
硬件的支持
這個問題顯然不是應用的代碼能夠解決的,因為上下文切換是OS決定的,普通應用無權干涉。但是硬件提供了一些指令原語,可以幫助我們解決這個問題。這些原語有test-and-set、compare-and-swap、fetch-and-add等等,我們可以基於這些原語來實現鎖信息修改的原子操作。例如,我們可以基於test-and-set進行實現:
//test-and-set的C代碼表示 int TestAndSet(int *ptr, int new) { int old = *ptr; //抓取舊值 *ptr = new; //設置新值 return old; //返回舊值 } typedef struct __lock_t { int flag; } lock_t; void init (lock_t *lock) { lock->flag = 0; } void lock(lock_t *lock) { //如果為1,說明原來就有人在用 //如果不為1,說明原來沒人在用,同時設置1,表明鎖現在歸我使用了 while (TestAndSet(&lock->flag, 1) == 1) ; //spin-wait (do noting) } void unlock (lock_t *lock) { lock->flag = 0; }
為什么這些指令不會被上下文切換所打斷?
上下文切換實際上也是執行切換的指令。CPU執行指令是一條一條執行的,test-and-set對於CPU來說就是一個指令,所以就算需要進行上下文切換,它也會先執行完當前的指令,然后再執行上下文切換的指令。