在多核系統中,會存在多個CPU核競爭同一資源的情形,這就必須有一些機制來保證在競爭中不會出現錯誤,即同步互斥機制。這裏主要針對同步互斥原語之一的自旋鎖進行一點分析和記錄。上圖爲一個多核系統的中斷部分,很顯然中斷部分會存在許多競爭相關問題。html
自旋鎖是用來在多處理器環境中工做的一種特殊的鎖,用於控制共享資源的訪問,是一種同步原語。當一個CPU正訪問自旋鎖保護的臨界區時,臨界區將被鎖上,其餘須要訪問此臨界區的CPU只能忙等待,即所謂的「自旋」,直到前面的CPU已訪問完臨界區,將臨界區解鎖。git
通常實現上須要定義spinlock的結構以及加解鎖的方式,伴隨實際應用中出現的問題,spinlock也不斷在改進。github
下面來分析下幾種Spinlock的實現以及改進方法:緩存
以下所示,爲wikipedia中提供的一種最簡單spinlock的x86彙編實現方式。分三個操做:併發
figure 1app
那麼問題來了,不是哪家強,而是對於這種spin_lock的實現,有兩點疑問須要想明白: less
問題一:爲何這樣設計的自旋鎖操做可以保證多個執行過程對共享資源互斥地訪問,或者說某一時刻只能有一個CPU得到鎖?高併發
關鍵就在於xchg指令。xchg是一個「原子」的交換指令,何謂「原子」?就是不可分割的意思,能夠參考個人另外一篇博客:Linux中同步互斥機制研究之原子操做。根據Intel手冊的描述,xchg指令在執行的時候會將CPU的LOCK位拉高,致使總線被鎖住,使得其餘的CPU不能使用總線,直到xchg指令執行結束纔將LOCK恢復,釋放訪問權限,經過這種方式保證了在執行xchg指令的時候只能由一個CPU獨享總線。知道了這點再去看代碼就明白了,當多個CPU均執行到spin_lock時,它們都想得到共享資源的訪問權限,執行到xchg指令的時候,總會有一個CPU率先執行xchg指令(宏觀上並行,微觀上必然仍是有前後順序),咱們姑且稱爲0號CPU,這時總線被鎖住,其餘CPU只能默默等待這個CPU執行完xchg指令。以後locked變量的值變爲1,0號CPU的ax寄存器值變爲0,spin_lock操做返回,程序繼續執行,因而0號CPU就進入了臨界區域,得到了某些共享資源的訪問權限。因爲locked變量的值爲1,其餘CPU執行完xchg指令後,ax寄存器的值仍爲1,因此只能jump回spin_lock,不能返回,從而保證在某一時刻只能有一個CPU得到鎖。性能
問題二:既然同一時刻只能有一個CPU獲取鎖,那麼誰應該獲取鎖?學習
對於這個問題,上述實現方式的答案是:隨機!並無使用任何方式去控制獲取鎖的前後順序。這樣的設計當然可以保證獨佔式地獲取鎖,並且全部的CPU最終都可以得到鎖,可是在實際應用中會引出另外一個問題。假設CPU C已經獲取了鎖,尚未釋放,這時CPU A嘗試獲取鎖失敗,自旋等待,過了很久CPU B也來嘗試獲取鎖,結果仍然失敗、自旋等待,此時CPU C釋放鎖,因爲獲取鎖的隨機性,CPU B獲取了鎖,而CPU A仍然要等待。理論上由於A是先來的,頗有可能A執行的任務比B重要,須要先獲取鎖,結果B後來反而先獲取了鎖,A須要再等待一段時間才能執行,若是足夠倒黴有可能長時間處於自旋等待狀態,甚至形成程序的邏輯錯誤。這就是自旋鎖中的「公平性」問題,事實上,沒有什麼系統使用這種方式實現自旋鎖。
針對「公平性」問題,很天然的想法就是全部CPU都應遵照必定的秩序,first come first serverd 就是一種不錯的策略。依照這種想法就須要保存CPU獲取鎖的前後順序,因而就有了Ticket spinlock:
figure 2
具體實現能夠參考locklessinc.com中提供的代碼,我在這裏沒有貼全,撿重要的說。在使用ticket lock時,owner和next域都須要初始化爲0,第一次獲取鎖的時候,next域被原子地加一,並返回next原來的值(爲0),因爲owner的值也爲0,因此線程獲得鎖,返回繼續執行,不然就會一直執行cpu_relax。解鎖過程也很簡單,barrier是內存屏障,保證barrier後的操做不會在barrier以前進行(這個涉及到memory reordering的內容),以後將owner加1,這樣順序上第二到達的線程就會從ticket_lock中返回繼續向下執行,對於後面來的線程依此類推。
#define barrier() asm volatile("": : :"memory")
#define cpu_relax() asm volatile("pause\n": : :"memory")
static inline void ticket_lock(ticketlock *t) { unsigned short me = atomic_xadd(&t->s.next, 1); while (t->s.owner!= me) cpu_relax(); } static inline void ticket_unlock(ticketlock *t) { barrier(); t->s.owner++; }
ticket spinlock解決了「公平性」問題,並且實現上也不復雜,因此不少系統中均採用ticket spinlock來控制共享資源的訪問,好比Linux和Rtems。然而ticket spinlock也有自身的缺陷,在併發性很高的系統中可能存在問題,下面來看另外一種自旋鎖。
MCS spinlock是Mellor-Crummey & Scott 在paper《Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors》中提出的,目的在於解決ticket lock中頻繁的緩存不命中問題。在高併發的系統中單純ticket spinlock可能並不能知足性能上的要求,緣由在於使用ticket spinlock時,全部執行線程均會在一個全局的「鎖變量」上自旋,形成頻繁的緩存不命中現象從而下降系統性能。
咱們知道CPU的每一個核都有本身的cache,當CPU處理數據時會首先從cache中查找,若cache中沒有才去內存中取,因此,若是讓每次須要處理的數據儘量地保存在cache中,就可以大幅提升系統的性能,由於從內存中讀的時鐘週期至少是從cache中讀的幾倍甚至幾百倍。因爲ticket lock使用的是全局鎖變量,所以每當鎖變量的值被修改後,全部CPU核的緩存將變爲無效,而爲了保證數據的一致性,又必須進行頻繁的緩存同步操做,致使系統性能降低。
MCS Spinlock在使用時,建立的是局部鎖變量,每一個線程都是在本身的局部鎖變量上自旋,避免了頻繁修改全局變量而引起的緩存不匹配問題。
figure 3
下面是MCS spinlock的實現,加解鎖操做的第一個參數爲指向全局鎖變量的指針,而第二個參數爲指向本地申請的鎖變量的指針。在獲取鎖的操做中因爲使用的是局部變量,因此最多隻會使得執行當前線程的CPU的cache 失效。
#ifndef _SPINLOCK_MCS #define _SPINLOCK_MCS
#define cmpxchg(P, O, N) __sync_val_compare_and_swap((P), (O), (N))
#define barrier() asm volatile("": : :"memory")
#define cpu_relax() asm volatile("pause\n": : :"memory")
static inline void *xchg_64(void *ptr, void *x) { __asm__ __volatile__("xchgq %0,%1" :"=r" ((unsigned long long) x) :"m" (*(volatile long long *)ptr), "0" ((unsigned long long) x) :"memory"); return x; } typedef struct mcs_lock_t mcs_lock_t; struct mcs_lock_t { mcs_lock_t *next; int spin; }; typedef struct mcs_lock_t *mcs_lock; static inline void lock_mcs(mcs_lock *m, mcs_lock_t *me) { mcs_lock_t *tail; me->next = NULL; me->spin = 0; tail = xchg_64(m, me); /* No one there? */
if (!tail) return; /* Someone there, need to link in */ tail->next = me; /* Make sure we do the above setting of next. */ barrier(); /* Spin on my spin variable */
while (!me->spin) cpu_relax(); return; } static inline void unlock_mcs(mcs_lock *m, mcs_lock_t *me) { /* No successor yet? */
if (!me->next) { /* Try to atomically unlock */
if (cmpxchg(m, me, NULL) == me) return; /* Wait for successor to appear */
while (!me->next) cpu_relax(); } /* Unlock next one */ me->next->spin = 1; } static inline int trylock_mcs(mcs_lock *m, mcs_lock_t *me) { mcs_lock_t *tail; me->next = NULL; me->spin = 0; /* Try to lock */ tail = cmpxchg(m, NULL, &me); /* No one was there - can quickly return */
if (!tail) return 0; return 1; // Busy
} #endif
K42是IBM的一個開源的研究性操做系統項目,裏面提供了另外一種Spinlock的實現方式,K42 spinlock在實現上與MCS spinlock相似,這裏再也不贅述,不一樣之處在於MCS spinlock使用的是local變量做爲是否等待的標誌而k42 spinlock中使用的是一個鏈表結構,這樣,就能夠避免傳遞額外的參數。
figure 4
實現代碼以下:
static inline void k42_lock(k42lock *l) { k42lock me; k42lock *pred, *succ; me.next = NULL; barrier(); pred = xchg_64(&l->tail, &me); if (pred) { me.tail = (void *) 1; barrier(); pred->next = &me; barrier(); while (me.tail) cpu_relax(); } succ = me.next; if (!succ) { barrier(); l->next = NULL; if (cmpxchg(&l->tail, &me, &l->next) != &me) { while (!me.next) cpu_relax(); l->next = me.next; } } else { l->next = succ; } } static inline void k42_unlock(k42lock *l) { k42lock *succ = l->next; barrier(); if (!succ) { if (cmpxchg(&l->tail, &l->next, NULL) == (void *) &l->next) return; while (!l->next) cpu_relax(); succ = l->next; } succ->tail = NULL; } static inline int k42_trylock(k42lock *l) { if (!cmpxchg(&l->tail, NULL, &l->next)) return 0; return 1; // Busy
}
博主在本身的虛擬機上對這幾種自旋鎖進行了簡單的性能測試,經過逐漸增長線程數來觀察spinlock的加解鎖性能,在測試程序中執行16000000對加解鎖操做,計算加解鎖之間的時間間隔(均以秒爲單位)。測試程序分別建立1個、2個、4個線程,逐漸增長線程數來觀察spinlock的加解鎖性能,對相同的臨界代碼段分別調用不一樣類型的自旋鎖,執行三次取平均值。以下圖所示爲測試程序流程:
figure 5
結果以下圖所示,在4核的虛擬機上分別以一、二、4線程運行,測試加解鎖的時間,右圖則是MCS論文中的原圖,能夠看出樓主的圖是其一個子集,趨勢基本符合,若是可以搭建NUMA系統進行後續性能測試工做,相信能對這些鎖的性能有一個更全面的認知。
figure 6
在工做中碰到了許多和同步互斥相關的問題,有不少都只是只知其一;不知其二,纔有了系統學習一下的衝動,遂成本文,後續有空可能還會寫兩篇關於同步互斥的文章。從網上找到了許多高質量的內容,感謝這些分享,堅信互聯網的核心就是 sharing & learning,如有什麼不許確的地方,歡迎指正。
[1]. http://locklessinc.com/articles/locks/
[2]. 何登成的技術博客
[3]. K42 github: https://github.com/jimix/k42
[4]. http://en.wikipedia.org/wiki/Spinlock
[5]. http://lwn.net/Articles/267968/