鎖的硬件實現

鎖是什麼

在多線程(協程)系統中,鎖是實現互斥訪問的同步機制。多線程

具體來講,鎖控制同一時刻只有一個線程(協程)訪問臨界區(critical section),避免競爭條件(race condition)的發生,從而保證併發的準確性。架構

具體的作法是在進入臨界區以前先加鎖,在離開臨界區以後立刻釋放鎖。併發

鎖的硬件實現

鎖是如何保證同一時刻只有一個線程(協程)訪問臨界區的,這涉及到鎖的具體實現。函數

通常來講,鎖的實現依賴底層的硬件指令,TAS(Test And Set)和 CAS(Compare And Set)是其中兩個被普遍使用的硬件指令。ui

Test And Set

TAS指令的語義是:向某個內存地址寫入值1,而且返回這塊內存地址存的原始值。TAS指令是原子的,這是由實現TAS指令的硬件保證的(這裏的硬件能夠是CPU,也能夠是實現了TAS的其餘硬件)。this

在x86架構中,TAS對應的彙編指令是bts(bit test and set),在多核CPU中,須要在前面加lock前綴,也就是lock btsspa

爲了便於理解把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並不會立刻刷到內存,下個線程也就不能立刻得到鎖,對鎖的效率有必定影響。

Compare And Swap

同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問題

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.
複製代碼
相關文章
相關標籤/搜索