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)。在任意時刻,只有一個線程被允許進入代碼保護區?;コ怄i實際上是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指令執行的原子性。
由于傳統自旋鎖無序競爭的本質特點,內核執行線程無法保證何時可以取到鎖,某些執行線程可能需要等待很長時間,導致鎖競爭不公平。
(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;elsereturn?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大小相同。
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。
總結
以上是生活随笔為你收集整理的C++性能优化(十二)——自旋锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kind多节点端口映射
- 下一篇: Navicat建数据库时字符集与排序规则