今天在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
本文僅用於學習和交流目的,不表明圖靈社區觀點。非商業轉載請註明做譯者、出處,並保留本文的原始連接。