基於隊列的鎖:mcs lock簡介

今天在Quora閒逛,看到一個對於MCS Lock的問答。答題的哥們深刻淺出的寫了一大篇,感受很是不錯,特意翻譯出來。html

原文翻譯node

要理解MCS Locks的本質,必須先知道其產生背景(Why),而後纔是其運做原理。就像原論文提到的,咱們先從spin-lock提及。spin-lock 是一種基於test-and-set操做的鎖機制。算法

<!-- lang: cpp -->
function Lock(lock){
    while(test_and_set(lock)==1);
}

function Unlock(lock){
    lock = 0;
}

test_and_set是一個原子操做,讀取lock,查看lock值,若是是0,設置其爲1,返回0。若是是lock值爲1, 直接返回1。這裏lock的值0和1分別表示無鎖和有鎖。因爲test_and_set的原子性,不會同時有兩個進程/線程同時進入該方法, 整個方法無須擔憂併發操做致使的數據不一致。緩存

一切看來都很完美,可是,有兩個問題:(1) test_and_set操做必須有硬件配合完成,必須在各個硬件(內存,二級緩存,寄存器)等等之間保持 數據一致性,通信開銷很大。(2) 他不保證公平性,也就是不會保證等待進程/線程按照FIFO的順序得到鎖,可能有比較倒黴的進程/線程等待很長時間 才能得到鎖。架構

爲了解決上面的問題,出現一種Ticket Lock的算法,比較相似於Lamport's backery algorithm。就像在麪包店裏排隊買麪包同樣,每一個人先付錢,拿 一張票,等待他手中的那張票被叫到。下面是僞代碼併發

<!-- lang: cpp -->
ticket_lock {
    int now_serving;
    int next_ticket;
};

function Lock(ticket_lock lock){
    //get your ticket atomically
    int my_ticket = fetch_and_increment(lock.next_ticket);
    while(my_ticket != now_serving){};    
}

function Unlock(ticket_lock lock){
    lock.now_serving++;
}

這裏用到了一個原子操做fetch_and_increment(實際上lock.now_serving++也必須保證是原子),這樣保證兩個進程/線程沒法獲得同一個ticket。 那麼上面的算法解決的是什麼問題呢?只調用一次原子操做!!!最原始的那個算法但是不停的在調用。這樣系統在保持一致性上的消耗就小不少。 第二,能夠按照先來先得(FIFO)的規則來得到鎖。沒有插隊,一切都很公平。學習

可是,這還不夠好。想一想多處理器的架構,每一個進程/線程佔用的處理器都在讀同一個變量,也就是now_serving。爲何這樣很差呢,從多個CPU緩存的 一致性考慮,每個處理器都在不停的讀取now_serving自己就有很多消耗。最後單個進程/線程處理器對now_serving++的操做不但要刷新到本地緩存中,並且 要與其餘的CPU緩存保持一致。爲了達到這個目的,系統會對全部的緩存進行一致性處理,讀取新值只能串行讀取,而後再作操做,整個讀取時間是與處理器個數 線性相關。fetch

說到這裏,纔會聊到mcs隊列鎖。使用mcs鎖的目的是,讓得到鎖的時間從O(n)變爲O(1)。每一個處理器都會有一個本地變量不用與其餘處理器同步。僞代碼以下atom

<!-- lang: cpp -->
mcs_node{
    mcs_node next;
    int is_locked;
}

mcs_lock{
    mcs_node queue;
}

function Lock(mcs_lock,mcs_node my_node){
    my_node.next = NULL;
    mcs_node predecessor = fetch_and_store(lock.queue,my_node);
    if(predecessor!=NULL){
        my_node.is_locked = true;
        predecessor.next = my_node;
        while(my_node.is_locked){};
    }
}

function Unlock(mcs_lock lock,mcs_node my_node){
    if(my_node.next == NULL){
        if(compare_and_swap(lock.queue,my_node,NULL){
            return ;
        }
        else{
            while(my_node.next == NULL){};
        }
    }

    my_node.next.is_locked = false;
}

此次代碼多了很多。可是隻要記住每個處理器在隊列裏都表明一個node,就不難理解整個邏輯。當咱們試圖得到鎖時,先將本身加入隊列,而後看看有沒有其餘 人(predecessor)在等待,若是有則等待本身的is_lock節點被修改爲false。當咱們解鎖時,若是有一個後繼的處理器在等待,則設置其is_locked=false,喚醒他。線程

在Lock方法裏,用fetch_and_store來將本地node加入隊列,該操做是個原子操做。若是發現前面有人在等待,則將本節點加入等待節點的next域中,等待前面的處理器喚醒本節點。 若是前面沒有人,那麼直接得到該鎖。

在Unlock方法中,首先查看是否有人排在本身後面。這裏要注意,即便暫時發現後面沒有人,也必須用原子操做compare_and_swap確認本身是最後的一個節點。若是不能確認 必須等待以後節點排(my_node.next == NULL)上來。最後設置my_node.next.is_locked = false喚醒等待者。

最後咱們看一下前面的問題是否解決了。原子操做的次數已經減小到最少,大多數時候只須要本地讀寫my_node變量。

註釋

1.原文來自於 http://www.quora.com/How-does-an-MCS-lock-work.

2.論文來自於 http://www.cs.rochester.edu/u/scott/papers/1991_TOCS_synch.pdf.

3.一些僞代碼 http://www.cs.rochester.edu/research/synchronization/pseudocode/ss.html

4.關於多處理器的架構下的共享變量訪問的機制能夠看看Java memory model或者搜一下NUMA與SMP架構,這方面本人也不是特別懂,還請各位賜教。

4.熟悉Java concurrent包的同窗能夠本身實現一下上面幾個算法,可用的類和方法有:

AtomicReferenceFieldUpdater.compareAndSet().對應compare_and_swap 

AtomicReferenceFieldUpdater.getAndSet().對應fetch_and_store

AtomicInteger.getAndIncrement().對應fetch_and_increment

本文僅用於學習和交流目的,不表明圖靈社區觀點。非商業轉載請註明做譯者、出處,並保留本文的原始連接。

相關文章
相關標籤/搜索