C++性能優化(十二)——自旋鎖
一、互斥鎖
1、互斥鎖簡介
互斥鎖屬於sleep-waiting類型鎖。Linux Kernel 2.6.x穩定版開始,Linux的互斥鎖都是futex (Fast Usermode Mutex)鎖。
Futex是一個在Linux上實現鎖定和構建高級抽象鎖如信號量和POSIX互斥的基本工具。
Futex由Hubertus Franke(IBM Thomas J. Watson 研究中心),Matthew Kirkwood,Ingo Molnar(Red Hat)和 Rusty Russell(IBM Linux 技術中心)等人創建。
Futex是由用戶空間的一個對齊的整型變量和附在其上的內核空間等待隊列構成。多進程或多線程絕大多數情況下對位於用戶空間的futex的整型變量進行操作(彙編語言調用CPU提供的原子操作指令來增加或減少),而其它情況下則需要通過代價較大的系統調用來對位於內核空間的等待隊列進行操作(如喚醒等待的進程/線程或將當前進程/線程放入等待隊列)。除了多個線程同時競爭鎖的少數情況外,基於futex的lock操作是不需要進行代價昂貴的系統調用操作的。
Futex核心思想是通過將大多數情況下非同時競爭lock的操作放到在用戶空間執行,而不是代價昂貴的內核系統調用方式來執行,從而提高了效率。
互斥鎖禁止多個線程同時進入受保護的代碼臨界區(critical section)。在任意時刻,只有一個線程被允許進入代碼保護區。互斥鎖實際上是count=1情況下的semaphore。
2、互斥鎖特點
互斥鎖缺點:
(1)等待互斥鎖會消耗時間,等待延遲會損害系統的可伸縮性。
(2)優先級倒置。低優先級的線程可以獲得互斥鎖,因此會阻礙需要同一互斥鎖的高優先級線程。
(3)鎖護送(lock convoying)。如果持有互斥鎖的線程分配的時間片結束,線程被取消調度,則等待同一互斥鎖的其它線程需要等待更長時間。
3、互斥鎖API
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
二、自旋鎖
1、自旋鎖簡介
自旋鎖(spin lock)屬於busy-waiting類型鎖。在多處理器環境中,自旋鎖最多隻能被一個可執行線程持有。如果一個可執行線程試圖獲得一個被其它線程持有的自旋鎖,那麼線程就會一直進行忙等待,自旋(空轉),等待自旋鎖重新可用。如果自旋鎖未被爭用,請求鎖的執行線程便立刻得到自旋鎖,繼續執行。
多處理器操作系統中某些資源是有限的,不同線程需要互斥訪問,因此需要引入鎖概念,只有獲取鎖的線程才能夠對資源進行訪問。多線程的核心是CPU的時間分片,同一時刻只能有一個線程獲取到鎖。對於沒有獲取到鎖的線程通常有兩種處理方式:自旋鎖,沒有獲取到鎖的線程會一直循環等待判斷資源是否已經釋放鎖,不用將線程阻塞起來;互斥鎖,把未獲取到鎖的線程阻塞起來,等待重新調度請求。
自旋鎖(spin lock)是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環。
獲取鎖的線程一直處於活躍狀態,但並沒有執行任何有效的任務,使用自旋鎖會造成busy-waiting。
2、自旋鎖的特點
自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快
非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖時需要從內核態恢復,導致線程在用戶態與內核態之間來回切換,嚴重影響鎖的性能。
3、自旋鎖原理
自旋鎖的原理比較簡單,如果持有鎖的線程能在短時間內釋放鎖資源,那麼等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞狀態,只需要等一等(自旋),等到持有鎖的線程釋放鎖後即可獲取,避免用戶進程和內核切換的消耗。
自旋鎖避免了操作系統進程調度和線程切換,通常適用在時間極短的情況,因此操作系統的內核經常使用自旋鎖。但如果長時間上鎖,自旋鎖會非常耗費性能。線程持有鎖時間越長,則持有鎖的線程被 OS調度程序中斷的風險越大。如果發生中斷情況,那麼其它線程將保持旋轉狀態(反覆嘗試獲取鎖),而持有鎖的線程並不打算釋放鎖,導致結果是無限期推遲,直到持有鎖的線程可以完成並釋放它爲止。
自旋鎖的目的是佔着CPU資源不進行釋放,等到獲取鎖立即進行處理。如果自旋執行時間太長,會有大量的線程處於自旋狀態佔用CPU資源,進而會影響整體系統的性能,因此可以給自旋鎖設定一個自旋時間,等時間一到立即釋放自旋鎖。
4、自旋鎖API
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
5、自旋鎖與互斥鎖
spinlock不會使線程狀態發生切換,mutex在獲取不到鎖的時候會選擇sleep。
spinlock優點:沒有耗時的系統調用,一直處於用戶態,執行速度快。
spinlock缺點:一直佔用CPU,而且在執行過程中還會鎖bus總線,鎖總線時其它處理器不能使用總線。
mutex獲取鎖分爲兩階段,第一階段在用戶態採用spinlock鎖總線的方式獲取一次鎖,如果成功立即返回;否則進入第二階段,調用系統的futex鎖去sleep,當鎖可用後被喚醒,繼續競爭鎖。
mutex優點:不會忙等,得不到鎖會sleep。
mutex缺點:sleep時會陷入到內核態,需要昂貴的系統調用。
三、自旋鎖實現
1、raw_spinlock
當某個處理器上的內核執行線程申請自旋鎖時,如果鎖可用,則獲得鎖,然後執行臨界區操作,最後釋放鎖;如果鎖已被佔用,線程並不會轉入睡眠狀態,而是忙等待該鎖,一旦鎖被釋放,則第一個感知此信息的線程將獲得鎖。
typedef struct {
unsigned int slock;
} raw_spinlock_t;
傳統自旋鎖本質是用一個整數來表示,值爲1代表鎖未被佔用,爲0或者爲負數表示被佔用。
在單處理機環境中可以使用特定的原子級彙編指令swap和test_and_set實現進程互斥,但由於中斷只能發生在兩條機器指令之間,而同一指令內的多個指令週期不可中斷,從而保證swap指令或test_and_set指令的執行不會交叉進行。
多處理器環境中利用test_and_set指令實現進程互斥,硬件需要提供進一步的支持,以保證test_and_set指令執行的原子性,目前多以鎖總線形式提供,由於test_and_set指令對內存的兩次操作都需要經過總線,在執行test_and_set指令前鎖住總線,在執行test_and_set指令後釋放總線,即可保證test_and_set指令執行的原子性。
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
__asm__ __volatile__(
__raw_spin_lock_string
:"=m" (lock->slock) : : "memory");
}
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
__asm__ __volatile__(
__raw_spin_unlock_string
);
}
由於傳統自旋鎖無序競爭的本質特點,內核執行線程無法保證何時可以取到鎖,某些執行線程可能需要等待很長時間,導致鎖競爭不公平。
(1)隨着處理器個數增加,自旋鎖競爭也在加劇,自然導致更長等待時間。釋放自旋鎖時的重置操作將無效化所有其它正在忙等待的處理器的緩存,那麼在處理器拓撲結構中臨近自旋鎖擁有者的處理器可能會更快地刷新緩存,因而增大獲得自旋鎖的機率。
(2)由於每個申請自旋鎖的處理器均在全局變量slock上忙等待,系統總線將因爲處理器間的緩存同步而導致繁重的流量,從而降低了系統整體性能。
2、ticket spinlock
Linux Kernel 2.6.25版本中引入了排隊自旋鎖,通過保存執行線程申請鎖的順序信息來解決不公平問題。
排隊自旋鎖仍然使用raw_spinlock_t 數據結構,但是賦予slock字段新含義。爲了保存順序信息,slock字段被分成兩部分Owner和Next,分別保存鎖持有者和未來鎖申請者的票據序號(Ticket Number),只有Owner和Next相等時,才表明鎖處於未使用狀態。
排隊自旋鎖初始化時slock被置爲0,即Owner和Next置爲0。Linux內核執行線程申請自旋鎖時,原子地將Next加1,並將原值返回作爲自己的票據序號。如果返回的票據序號等於申請時Owner值,說明自旋鎖處於未使用狀態,則直接獲得鎖;否則,線程忙等待檢查Owner是否等於自己持有的票據序號,一旦相等,則表明鎖輪到自己獲取。線程釋放鎖時,原子地將Owner加1即可,下一個線程將會發現這一變化,從忙等待狀態中退出。線程將嚴格地按照申請順序依次獲取排隊自旋鎖,從而完全解決了不公平問題。
typedef struct arch_spinlock {
union {
__ticketpair_t head_tail;
struct __raw_tickets {
__ticket_t head, tail;
} tickets;
};
} arch_spinlock_t;
申請自旋鎖時,原子地將tail加1,釋放時,head加1。只有head域和tail域的值相等時,才表明鎖處於未使用的狀態。
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
asm volatile("\n1:\t"
LOCK_PREFIX " ; decb %0\n\t"
"jns 3f\n"
"2:\t"
"rep;nop\n\t"
"cmpb $0,%0\n\t"
"jle 2b\n\t"
"jmp 1b\n"
"3:\n\t"
: "+m" (lock->slock) : : "memory");
}
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");
}
在大規模多處理器系統和NUM系統中,排隊自旋鎖(包括傳統自旋鎖)存在一個比較嚴重的性能問題:由於執行線程均在同一個共享變量slock上自旋,申請和釋放鎖的時候必須對slock進行修改,將導致所有參與排隊自旋鎖操作的處理器的緩存變得無效。如果排隊自旋鎖競爭比較激烈的話,頻繁的緩存同步操作會導致繁重的系統總線和內存的流量,從而大大降低了系統整體的性能。
3、mcs spinlock
每個鎖的申請者(處理器)只在一個本地變量上自旋。MCS Spinlock是一種基於鏈表結構的自旋鎖。
MCS Spinlock的設計目標如下:
(1)保證自旋鎖申請者以先進先出的順序獲取鎖(FIFO)
(2)只在本地可訪問的標誌變量上自旋。
(3)在處理器個數較少的系統中或鎖競爭並不激烈的情況下,保持較高性能。
(4)自旋鎖的空間複雜度(即鎖數據結構和鎖操作所需的空間開銷)爲常數。
(5)在沒有處理器緩存一致性協議保證的系統中也能很好地工作。
MCS Spinlock採用鏈表結構將全體鎖申請者的信息串成一個單向鏈表。每個鎖申請者必須提前分配一個本地mcs_lock_node,其中至少包括2個字段:本地自旋變量waiting和指向下一個申請者 mcs_lock_node結構的指針變量next。waiting初始值爲1,申請者自旋等待其直接前驅釋放鎖;爲0時結束自旋。
自旋鎖數據結構mcs_lock是一個永遠指向最後一個申請者 mcs_lock_node的指針,當且僅當鎖處於未使用(無任何申請者)狀態時爲NULL值。MCS Spinlock依賴原子的swap和CAS(compare_and_swap)操作,如果缺乏CAS支持,MCS Spinlock 就不能保證以先進先出的順序獲取鎖。
每個鎖有NR_CPUS個元素node數組,mcs_lock_node結構可以在處理器所處節點的內存中分配,從而加快訪問速度。
typedef struct _mcs_lock_node {
volatile int waiting;
struct _mcs_lock_node *volatile next;
} ____cacheline_aligned_in_smp mcs_lock_node;
typedef mcs_lock_node *volatile mcs_lock;
typedef struct {
mcs_lock slock;
mcs_lock_node nodes[NR_CPUS];
} raw_spinlock_t;
static __always_inline void __raw_spin_lock(raw_spinlock_t *lock)
{
int cpu;
mcs_lock_node *me;
mcs_lock_node *tmp;
mcs_lock_node *pre;
cpu = raw_smp_processor_id();
me = &(lock->nodes[cpu]);
tmp = me;
me->next = NULL;
pre = xchg(&lock->slock, tmp);
if (pre == NULL) {
/* mcs_lock is free */
return;
}
me->waiting = 1;
smp_wmb();
pre->next = me;
while (me->waiting) {
asm volatile ("pause");
}
}
static __always_inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
int cpu;
mcs_lock_node *me;
cpu = raw_smp_processor_id();
me = &(lock->nodes[cpu]);
me->next = NULL;
if (cmpxchg(&lock->slock, NULL, me) == NULL)
return 1;
else
return 0;
}
static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
int cpu;
mcs_lock_node *me;
mcs_lock_node *tmp;
cpu = raw_smp_processor_id();
me = &(lock->nodes[cpu]);
tmp = me;
if (me->next == NULL) {
if (cmpxchg(&lock->slock, tmp, NULL) == me) {
/* mcs_lock I am the last. */
return;
}
while (me->next == NULL)
continue;
}
/* mcs_lock pass to next. */
me->next->waiting = 0;
}
mcs spinlock 鎖佔用空間大。
4、qspinlock
qspinlock在Linux Kernel 4.2引入,基於mcs spinlock設計思想但解決了mcs spinlock接口不一致或空間太大的問題。
qspinlock數據結構體比mcs lock大大減小,與ticket spinlock大小相同。
struct __qspinlock {
union {
atomic_t val;
#ifdef __LITTLE_ENDIAN
struct {
u8 locked;
u8 pending;
};
struct {
u16 locked_pending;
u16 tail;
};
#else
struct {
u16 tail;
u16 locked_pending;
};
struct {
u8 reserved[2];
u8 pending;
u8 locked;
};
#endif
static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
u32 val;
val = atomic_cmpxchg_acquire(&lock->val, 0, _Q_LOCKED_VAL);
if (likely(val == 0))
return;
queued_spin_lock_slowpath(lock, val);
}
qspinlock採用mcs lock機制, 每一個CPU都定義有一個struct mcs spinlock數據結構,在大規模多處理器系統和NUM架構中, 使用qspinlock可以較好的提高鎖的性能。
5、性能比較
寫一個spinlock的性能測試驅動,在等待相同時間後比較spinlock 臨界區域的值, 從而比較各個鎖的性能差異。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/kernel/h>
#include <linux/spinlock.h>
#include <linux/random.h>
#include <linux/slab.h>
#incude <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/atomic.h>
int spinlock_num;
struct worker {
int burns;
struct task_struct *task;
}
static struct worker *workers;
static int threads = 2;
module_param(threads, int, 0);
static spinlock_t lock;
static int runtime = 10;
module_param(runtime, int, 0);
static int bench_running;
static task_struct *monitor_task;
static int rerun, done;
module_param(rerun, int, S_IRUGO|S_ISUSR);
module_param(done, int, S_IRUGO|S_ISUSR);
static int work(void *data)
{
struct worker *wk = (struct worker*)arg;
while(!kthread_should_stop()) {
cond_resched();
if (!ACCESS_ONCE(bench_running))
continue;
spin_lock(&lock)
spinlock_num++;
spin_unlock(&lock);
}
return 0;
}
static int monitor(void *unused)
{
int i, c;
int total, min, max, avg;
repeat:
total = 0, min = INT_MAX, max = 0, avg = 0;
spinlock_num = 0;
workers = (struct worker *)kzalloc(sizeof(struct worker) * threads, GFP_KERNEL);
for (i = 0; i < threads; i++) {
c = i %num_online_cpus();
workers[i].task = kthread_create(work, &workers, "locktest/%d:%d", c, i);
kthread_bind(workers[i].task, c);
wake_up_process(workers[i].task);
}
bench_running = 0;
for (i = 0; i < threads; i++) {
if (workers[i].task)
kthread_stop(workers[i].task);
}
kfree(workers);
printk("lockresult:%6d %8d %12d\n", num_online_cpus(), threads, spinlock_num);
done = 1;
while(!kthread_should_stop()) {
schedule_timeout(1);
if (cmpxchg(&rerun, done, 0)) {
done = 0;
goto repeat;
}
}
return 0;
}
static int locktest_init(void)
{
monitor_task = kthread_run(monitor, NULL, "monitor");
return 0;
}
static void locktest_exit(void)
{
kthread_stop(monitor_task);
}
module_init(locktest_init);
module_exit(locktest_exit);
MODULE_LICENSE("GPL");
在CPU較少的情況下, qspinlock的性能和ticket spinlock的性能差不多, 在CPU較多的情況下,qspinlock的性能遠好於ticket spinlock。