【雜談】從底層看鎖的實現

如下內容針對互斥鎖。數據結構

爲何須要鎖?

鎖表明着對臨界區的訪問權限。只有得到鎖的操做對象,才能進入臨界區。分佈式

鎖的本質是什麼?

鎖的本質是一個數據結構(或者說是一個對象),這個對象內保留着描述鎖所須要的必要信息。如當前鎖是否已被佔用,被哪一個線程佔用。而鎖的一些工具,函數庫,實際上就是對一個鎖對象的信息進行變動。函數

上鎖操做    =>  嘗試對鎖對象的信息進行修改,若是修改爲功,則程序繼續向下執行,不然將暫時停留在此。(停留的方式有兩種,一種是自旋反覆嘗試,另外一種是掛起等待喚醒)工具

解鎖操做    =>  重置鎖對象的信息。fetch

相似下面這樣(注:這個例子不許確,後面會講spa

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中),這樣其餘電腦的應用也能夠對這個鎖進行訪問,這就是分佈式鎖。code

對鎖信息進行修改

存在的問題對象

前面有提到,前面的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來講就是一個指令,因此就算須要進行上下文切換,它也會先執行完當前的指令,而後再執行上下文切換的指令。

相關文章
相關標籤/搜索