
在多核系統中,會存在多個CPU核競爭同一資源的情形,這就必須有一些機制來保證在競爭中不會出現錯誤,即同步互斥機制。這里主要針對同步互斥原語之一的自旋鎖進行一點分析和記錄。上圖為一個多核系統的中斷部分,很顯然中斷部分會存在許多競爭相關問題。
自旋鎖(Spinlock)
自旋鎖是用來在多處理器環境中工作的一種特殊的鎖,用於控制共享資源的訪問,是一種同步原語。當一個CPU正訪問自旋鎖保護的臨界區時,臨界區將被鎖上,其他需要訪問此臨界區的CPU只能忙等待,即所謂的“自旋”,直到前面的CPU已訪問完臨界區,將臨界區解鎖。
一般實現上需要定義spinlock的結構以及加解鎖的方式,伴隨實際應用中出現的問題,spinlock也不斷在改進。
下面來分析下幾種Spinlock的實現以及改進方法:
1.A simple spinlock
如下所示,為wikipedia中提供的一種最簡單spinlock的x86匯編實現方式。分三個操作:
- 定義全局locked變量的值為0;
- 定義spin_lock操作,給累加器ax賦值為1,交換ax和locked變量的值,然后檢測ax的值看是否為零,若為零則繼續向下執行ret返回,相當於一個獲取鎖的過程;若不為零則說明鎖已被其他執行過程獲得,跳轉回spin_lock繼續執行,如此就形成了一個循環執行的效果,即稱之為“自旋”;
- 定義spin_unlock操作,給累加器ax賦值為0,交換ax與locked的值,然后返回,相當於一個解鎖的過程。

figure 1
那么問題來了,不是哪家強,而是對於這種spin_lock的實現,有兩點疑問需要想明白:
問題一:為什么這樣設計的自旋鎖操作能夠保證多個執行過程對共享資源互斥地訪問,或者說某一時刻只能有一個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需要再等待一段時間才能執行,如果足夠倒霉有可能長時間處於自旋等待狀態,甚至造成程序的邏輯錯誤。這就是自旋鎖中的“公平性”問題,事實上,沒有什么系統使用這種方式實現自旋鎖。
2.Ticket spinlock
針對“公平性”問題,很自然的想法就是所有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也有自身的缺陷,在並發性很高的系統中可能存在問題,下面來看另一種自旋鎖。
3.MCS 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
4.K42 spinlock
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
}
5.性能
博主在自己的虛擬機上對這幾種自旋鎖進行了簡單的性能測試,通過逐漸增加線程數來觀察spinlock的加解鎖性能,在測試程序中執行16000000對加解鎖操作,計算加解鎖之間的時間間隔(均以秒為單位)。測試程序分別創建1個、2個、4個線程,逐漸增加線程數來觀察spinlock的加解鎖性能,對相同的臨界代碼段分別調用不同類型的自旋鎖,執行三次取平均值。如下圖所示為測試程序流程:

figure 5
結果如下圖所示,在4核的虛擬機上分別以1、2、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/
