mutex一般用於為一段程式碼加鎖,以保證這段程式碼的原子性(atomic)操作,即:要麼不執行這段程式碼,要麼將這段程式碼全部執行完畢。
例如,最簡單的併發衝突問題就是一個變數自增1:
balance = balance + 1;
表面看這是一條語句,可是在背後的彙編中我們可以看到,指令集操作過程中會引入中間變數來儲存右邊的值,進而這個操作至少會被擴充為:
int tmp = balance + 1;
balance = tmp;
這就需要一把互斥鎖(mutual exclusive, mutex)將這段程式碼給鎖住,使其達到任何一個執行緒“要麼全部執行上述程式碼,要麼不執行這段程式碼”的效果。這個用法可以表示為:
lock_t mutex;
...
lock(&mutex)
balance = balance + 1;
unlock(&mutex);
那麼,一個自然的問題便是,我如何實現上面的這個lock()
函式呢?
乍一看這個問題是非常複雜的,特別是考慮到它能夠被適用於各種程式碼的各種情況。但經過各種簡化,這個lock()
實現,可以通過幾個test和set的組合得以實現。
例如,
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;
}
我第一次看到這個演算法的時候非常驚訝,一個本來極其複雜的問題就這麼優雅地被解決了。它僅僅涉及到對條件的檢驗和變數的複製,然後整個問題就這麼輕而易舉地被攻破了。
當然,我並沒能看到上述程式碼的“坑”,也即是必須依靠指令集級別的支援才能真正做到atomic。這同樣說明了併發程式的困難,稍微不注意便會調入一個萬劫不復的坑裡,並且你還不知道哪裡出錯了。
上述極端優雅的程式碼,有一個隱藏的坑,那便是在lock()
函式的實現裡,while
迴圈那一段其實是可以被亂入的。
假設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,彈簧鎖。之所以叫彈簧鎖,那是因為在各類鎖當中,彈簧鎖就是最初的被投入工業使用的最簡單的實現技術。