互斥鎖mutex的簡單實現

mutex通常用於爲一段代碼加鎖,以保證這段代碼的原子性(atomic)操做,即:要麼不執行這段代碼,要麼將這段代碼所有執行完畢。算法

例如,最簡單的併發衝突問題就是一個變量自增1:併發

balance = balance + 1;

表面看這是一條語句,但是在背後的彙編中咱們能夠看到,指令集操做過程當中會引入中間變量來保存右邊的值,進而這個操做至少會被擴充爲:函數

int tmp = balance + 1;
balance = tmp;

這就須要一把互斥鎖(mutual exclusive, mutex)將這段代碼給鎖住,使其達到任何一個線程「要麼所有執行上述代碼,要麼不執行這段代碼」的效果。這個用法能夠表示爲:atom

lock_t mutex;
...
lock(&mutex)
    balance = balance + 1;
unlock(&mutex);

那麼,一個天然的問題即是,我如何實現上面的這個lock()函數呢?線程

乍一看這個問題是很是複雜的,特別是考慮到它可以被適用於各類代碼的各類狀況。但通過各類簡化,這個lock()實現,能夠經過幾個test和set的組合得以實現。code

例如,it

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *mutex) {
    // 0: lock is available
    // 1: lock is held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (mutex->flag == 1) {  // Test the flag.
        ;    // Wait the lock
    mutex->flag = 1;  // Set the lock, i.e. start to hold lock
}

void unlock(lock_t *mutex) {
    mutex->flag = 0;
}

我第一次看到這個算法的時候很是驚訝,一個原本極其複雜的問題就這麼優雅地被解決了。它僅僅涉及到對條件的檢驗和變量的複製,而後整個問題就這麼垂手可得地被攻破了。class

固然,我並沒能看到上述代碼的「坑」,也便是必須依靠指令集級別的支持才能真正作到atomic。這一樣說明了併發程序的困難,稍微不注意便會調入一個萬劫不復的坑裏,而且你還不知道哪裏出錯了。thread

上述極端優雅的代碼,有一個隱藏的坑,那即是在lock()函數的實現裏,while循環那一段實際上是能夠被亂入的。test

假設thread A是第一個運行到此的線程,那麼它獲得的mutex->flag就確定是0,因而它繼續跳出循環往下運行,但願經過下面的mutex->flag = 1來持有鎖,使得其它線程在檢測while循環時爲真,進而進入循環的等待狀態。

可若是在A執行到這個賦值爲1的語句以前,又有另一個thread B運行到了這個while循環部分,因爲mutex->flag還未被賦值爲1,B一樣能夠跳出while,從而跟A同樣拿到這把鎖!這就出現了衝突。

那怎麼辦呢?仔細後能夠發現,其實關鍵問題就在於:

  • mutex->flag的檢測
  • mutex->flag的賦值

這兩個操做必須是不被幹擾的,也就是它必須是atomic的,要麼這兩段代碼不被執行,要麼這兩段代碼被不中斷地完整執行。

這就須要藉助CPU指令集的幫助,來保證上述兩條語句的atomic操做,也便是著名的TestAndSet()操做。

int TestAndSet(int *ptr, int new) {
    int old = *ptr;
    *ptr = new;
    return old;
}

CPU的指令集,並不須要支持繁複的各類atomic操做。僅僅支持上面這個函數,各類互斥加鎖的情形,便都可以被涵蓋。

此時,在回到咱們最開始的那個優雅的lock()實現,就能夠將其改造爲:

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *lock) {
    // 0: lock is available
    // 1: lock is held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (TestAndSet(&lock_t->flag, 1) == 1) {
        ;
}

void unlock(lock_t *lock) {
    lock->flag = 0;
}

上述代碼極其精巧。乍一看在lock()實現裏不是還缺乏一行mutex->flag = 1;麼?可其實呢,它已經被整合到了TestAndSet()函數中。

這樣的支持TestAndSet()的實現,即是最簡單的spin lock,彈簧鎖。之因此叫彈簧鎖,那是由於在各種鎖當中,彈簧鎖就是最初的被投入工業使用的最簡單的實現技術。

相關文章
相關標籤/搜索