在多線程(協程)系統中,鎖是實現互斥訪問的同步機制。多線程
具體來講,鎖控制同一時刻只有一個線程(協程)訪問臨界區(critical section),避免競爭條件(race condition)的發生,從而保證併發的準確性。架構
具體的作法是在進入臨界區以前先加鎖,在離開臨界區以後立刻釋放鎖。併發
鎖是如何保證同一時刻只有一個線程(協程)訪問臨界區的,這涉及到鎖的具體實現。函數
通常來講,鎖的實現依賴底層的硬件指令,TAS(Test And Set)和 CAS(Compare And Set)是其中兩個被普遍使用的硬件指令。ui
TAS指令的語義是:向某個內存地址寫入值1,而且返回這塊內存地址存的原始值。TAS指令是原子的,這是由實現TAS指令的硬件保證的(這裏的硬件能夠是CPU,也能夠是實現了TAS的其餘硬件)。this
在x86架構中,TAS對應的彙編指令是bts(bit test and set),在多核CPU中,須要在前面加lock前綴,也就是lock bts
。spa
爲了便於理解把TAS翻譯成僞代碼線程
function TestAndSet(boolean_ref lock) {
boolean initial = lock;
lock = true;
return initial;
}
複製代碼
注意TestAndSet函數的執行要是原子的。翻譯
那麼怎麼在TAS的基礎上實現鎖呢?下面的case在TAS的基礎上實現一個簡單的自旋鎖,這個鎖雖然簡單,在功能上是完備的,關於鎖的效率和公平性問題後面再討論。code
volatile int lock = 0;
void Critical() {
while (TestAndSet(&lock) == 1);
critical section // only one process can be in this section at a time
lock = 0 // release lock when finished with the critical section
}
複製代碼
注意上面的volatile
關鍵字,volatile
的語義是直接從內存中讀取變量值。 對於實現了memory barriers
的編譯器來講,每次讀取變量值以前,都會把以前對變量的寫操做所有刷入內存,對於沒有實現memory barriers
的編譯器來講則不必定,這種狀況下,上述釋放鎖的操做lock=0
,不會當即生效,雖然上個線程已經釋放了鎖,可是lock=0
並不會立刻刷到內存,下個線程也就不能立刻得到鎖,對鎖的效率有必定影響。
同TAS同樣,CAS也是由硬件支持的原子操做。在x86架構中,CAS對應的彙編指令是CMPXCHG。
CAS的語義是:比較某個內存地址的值與一個給定值(這個給定值是上一刻今後內存地址讀出來的),若是相等,則把一個新值寫入到此內存地址,有點抽象,翻譯成代碼以下
int compare_and_swap(int* reg, int oldval, int newval) {
ATOMIC();
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return old_reg_val;
}
複製代碼
與TAS不一樣的是,TAS只操做一個bit,CAS能夠操做32(64)個bit。
在多核CPU環境下,在CMPXCHG指令前加LOCK前綴,LOCK CMPXCHG
才能保證操做是原子的。
ABA問題也叫作調包問題,其含義是:在多核CPU中,若是在讀舊值和CAS操做之間,另一個或者多個核對舊值就好了兩次或屢次修改,且修改的最終結果與舊值相同(從A到B再到A),那麼在執行CAS操做的核看來,CAS操做成功了,可是正確的行爲應該是這次CAS操做失敗,由於舊值已經被修改屢次。
解決ABA問題的一個思路是使用double-length CAS,一半字節用來存儲一個counter,一半字節存儲value,每次對value進行修改,counter都會+1。這樣在發生ABA時,雖然value是同樣的,可是counter大機率(思考什麼狀況下counter也會一致)不一致,從而解決ABA問題。
須要注意的是,發生ABA問題的根本緣由是一個核在執行CAS操做時(假設使用內存地址X),其餘核是能夠讀寫內存地址X的,也就是說,在單核CPU中,不會發生ABA問題。在多核CPU中,除了double-length CAS以外,還有其餘方式防止ABA。好比前面說的,在CMPXCHG指令前加LOCK前綴,也能防止ABA出現。LOCK CMPXCHG
中LOCK的語義是保證LOCK後續指令訪問對應內存是排他的,都不容許其餘核訪問對應的內存了,天然也就不存在ABA問題了。
最後須要說明的是,也存在不依賴於TAS和CAS實現的鎖,好比說下面的case。
; Intel syntax
locked: ; The lock variable. 1 = locked, 0 = unlocked.
dd 0
spin_lock:
mov eax, 1 ; Set the EAX register to 1.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
; This will always store 1 to the lock, leaving
; the previous value in the EAX register.
test eax, eax ; Test EAX with itself. Among other things, this will
; set the processor's Zero Flag if EAX is 0.
; If EAX is 0, then the lock was unlocked and
; we just locked it.
; Otherwise, EAX is 1 and we didn't acquire the lock.
jnz spin_lock ; Jump back to the MOV instruction if the Zero Flag is
; not set; the lock was previously locked, and so
; we need to spin until it becomes unlocked.
ret ; The lock has been acquired, return to the calling
; function.
spin_unlock:
xor eax, eax ; Set the EAX register to 0.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
ret ; The lock has been released.
複製代碼