下面我會(huì)分析一下自旋鎖,并代碼實(shí)現(xiàn)自旋鎖和互斥鎖的性能對(duì)比,以及利用C++11實(shí)現(xiàn)自旋鎖。
一:自旋鎖(spin lock)
自旋鎖是一種用于保護(hù)多線程共享資源的鎖,與一般互斥鎖(mutex)不同之處在于當(dāng)自旋鎖嘗試獲取鎖時(shí)以忙等待(busy waiting)的形式不斷地循環(huán)檢查鎖是否可用。
在多CPU的環(huán)境中, 對(duì)持有鎖較短的程序來(lái)說(shuō),使用自旋鎖代替一般的互斥鎖往往能夠提高程序的性能。
最后加粗的句子很重要,本文將針對(duì)該結(jié)論進(jìn)行驗(yàn)證。
下面是man手冊(cè)中對(duì)自旋鎖pthread_spin_lock()函數(shù)的描述:
DESCRIPTION The pthread_spin_lock() function shall lock the spin lock referenced by lock. The calling thread shall acquire the lock if it is not held by another thread. Otherwise, the thread shall spin (that is, shall not return from the pthread_spin_lock() call) until the lock becomes available. The results are undefined if the calling thread holds the lock at the time the call is made. The pthread_spin_trylock() function shall lock the spin lock referenced by lock if it is not held by any thread. Otherwise, the function shall fail. The results are undefined if any of these functions is called with an uninitialized spin lock.
可以看出,自選鎖的主要特征:當(dāng)自旋鎖被一個(gè)線程獲得時(shí),它不能被其它線程獲得。如果其他線程嘗試去phtread_spin_lock()獲得該鎖,那么它將不會(huì)從該函數(shù)返回,而是一直自旋(spin),直到自旋鎖可用為止。
使用自旋鎖時(shí)要注意:
- 由于自旋時(shí)不釋放CPU,因而持有自旋鎖的線程應(yīng)該盡快釋放自旋鎖,否則等待該自旋鎖的線程會(huì)一直在哪里自旋,這就會(huì)浪費(fèi)CPU時(shí)間。
- 持有自旋鎖的線程在sleep之前應(yīng)該釋放自旋鎖以便其他咸亨可以獲得該自旋鎖。內(nèi)核編程中,如果持有自旋鎖的代碼sleep了就可能導(dǎo)致整個(gè)系統(tǒng)掛起。(下面會(huì)解釋)
使用任何鎖都需要消耗系統(tǒng)資源(內(nèi)存資源和CPU時(shí)間),這種資源消耗可以分為兩類:
1.建立鎖所需要的資源
2.當(dāng)線程被阻塞時(shí)所需要的資源
POSIX提供的與自旋鎖相關(guān)的函數(shù)有以下幾個(gè),都在中。
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
初始化spin lock, 當(dāng)線程使用該函數(shù)初始化一個(gè)未初始化或者被destroy過(guò)的spin lock有效。該函數(shù)會(huì)為spin lock申請(qǐng)資源并且初始化spin lock為unlocked狀態(tài)。
有關(guān)第二個(gè)選項(xiàng)是這么說(shuō)的:
If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_SHARED, the implementation shall permit the spin lock to be operated upon by any thread that has access to the memory where the spin lock is allocated, even if it is allocated in memory that is shared by multiple processes. If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_PRIVATE, or if the option is not supported, the spin lock shall only be operated upon by threads created within the same process as the thread that initialized the spin lock. If threads of differing processes attempt to operate on such a spin lock, the behav‐ ior is undefined.
所以,如果初始化spin lock的線程設(shè)置第二個(gè)參數(shù)為PTHREAD_PROCESS_SHARED,那么該spin lock不僅被初始化線程所在的進(jìn)程中所有線程看到,而且可以被其他進(jìn)程中的線程看到,PTHREAD_PROESS_PRIVATE則只被同一進(jìn)程中線程看到。如果不設(shè)置該參數(shù),默認(rèn)為后者。
int pthread_spin_destroy(pthread_spinlock_t *lock);
銷毀spin lock,作用和mutex的相關(guān)函數(shù)類似,就不翻譯了:
The pthread_spin_destroy() function shall destroy the spin lock referenced by lock and release any resources used by the lock. The effect of subsequent use of the lock is undefined until the lock is reinitialized by another call to pthread_spin_init(). The results are undefined if pthread_spin_destroy() is called when a thread holds the lock, or if this function is called with an uninitialized thread spin lock.
不過(guò)和mutex的destroy函數(shù)一樣有這樣的性質(zhì)(當(dāng)初害慘了我):
The result of referring to copies of that object in calls to pthread_spin_destroy(), pthread_spin_lock(), pthread_spin_try‐ lock(), or pthread_spin_unlock() is undefined.
int pthread_spin_lock(pthread_spinlock_t *lock);
加鎖函數(shù),功能上文都說(shuō)過(guò)了,不過(guò)這么一點(diǎn)值得注意:
EBUSY A thread currently holds the lock. These functions shall not return an error code of [EINTR].
int pthread_spin_trylock(pthread_spinlock_t *lock);
還有這個(gè)函數(shù),這個(gè)一般很少用到。
int pthread_spin_unlock(pthread_spinlock_t *lock);
解鎖函數(shù)。不是持有鎖的線程調(diào)用或者解鎖一個(gè)沒(méi)有l(wèi)ock的spin lock這樣的行為都是undefined的。
二:自旋鎖和互斥鎖的區(qū)別
從實(shí)現(xiàn)原理上來(lái)講,Mutex屬于sleep-waiting類型的 鎖。例如在一個(gè)雙核的機(jī)器上有兩個(gè)線程(線程A和線程B),它們分別運(yùn)行在Core0和Core1上。假設(shè)線程A想要通過(guò) pthread_mutex_lock操作去得到一個(gè)臨界區(qū)的鎖,而此時(shí)這個(gè)鎖正被線程B所持有,那么線程A就會(huì)被阻塞(blocking),Core0 會(huì)在此時(shí)進(jìn)行上下文切換(Context Switch)將線程A置于等待隊(duì)列中, 此時(shí)Core0就可以運(yùn)行其他的任務(wù)(例如另一個(gè)線程C)而不必進(jìn)行忙等待。而Spin lock則不然,它屬于busy-waiting類型的鎖,如果線程A是使用pthread_spin_lock操作去請(qǐng)求鎖,那么線程A就會(huì)一直在 Core0上進(jìn)行忙等待并不停的進(jìn)行鎖請(qǐng)求,直到得到這個(gè)鎖為止。
如果大家去查閱Linux glibc中對(duì)pthreads API的實(shí)現(xiàn)NPTL(Native POSIX Thread Library) 的源碼的話(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我們系統(tǒng)中NPTL的版本號(hào)),就會(huì)發(fā)現(xiàn)pthread_mutex_lock()操作如果 沒(méi)有鎖成功的話就會(huì)調(diào)用system_wait()的系統(tǒng)調(diào)用并將當(dāng)前線程加入該mutex的等待隊(duì)列里。而spin lock則可以理解為在一個(gè)while(1)循環(huán)中用內(nèi)嵌的匯編代碼實(shí)現(xiàn)的鎖操作(印象中看過(guò)一篇論文介紹說(shuō)在linux內(nèi)核中spin lock操作只需要兩條CPU指令,解鎖操作只用一條指令就可以完成)。有興趣的朋友可以參考另一個(gè)名為sanos的微內(nèi)核中pthreds API的實(shí)現(xiàn):mutex.c spinlock.c,盡管與NPTL中的代碼實(shí)現(xiàn)不盡相同,但是因?yàn)樗膶?shí)現(xiàn)非常簡(jiǎn)單易懂,對(duì)我們理解spin lock和mutex的特性還是很有幫助的。
對(duì)于自旋鎖來(lái)說(shuō),它只需要消耗很少的資源來(lái)建立鎖;隨后當(dāng)線程被阻塞時(shí),它就會(huì)一直重復(fù)檢查看鎖是否可用了,也就是說(shuō)當(dāng)自旋鎖處于等待狀態(tài)時(shí)它會(huì)一直消耗CPU時(shí)間。
對(duì)于互斥鎖來(lái)說(shuō),與自旋鎖相比它需要消耗大量的系統(tǒng)資源來(lái)建立鎖;隨后當(dāng)線程被阻塞時(shí),線程的調(diào)度狀態(tài)被修改,并且線程被加入等待線程隊(duì)列;最后當(dāng)鎖可用 時(shí),在獲取鎖之前,線程會(huì)被從等待隊(duì)列取出并更改其調(diào)度狀態(tài);但是在線程被阻塞期間,它不消耗CPU資源。
因此自旋鎖和互斥鎖適用于不同的場(chǎng)景。自旋鎖適用于那些僅需要阻塞很短時(shí)間的場(chǎng)景,而互斥鎖適用于那些可能會(huì)阻塞很長(zhǎng)時(shí)間的場(chǎng)景。
三:自旋鎖與linux內(nèi)核進(jìn)程調(diào)度關(guān)系
現(xiàn)在我們就來(lái)說(shuō)一說(shuō)之前的問(wèn)題,如果臨界區(qū)可能包含引起睡眠的代碼則不能使用自旋鎖,否則可能引起死鎖:
那么為什么信號(hào)量保護(hù)的代碼可以睡眠而自旋鎖會(huì)死鎖呢?
先看下自旋鎖的實(shí)現(xiàn)方法吧,自旋鎖的基本形式如下:
spin_lock(&mr_lock):
//critical region
spin_unlock(&mr_lock);
跟蹤一下spin_lock(&mr_lock)的實(shí)現(xiàn)
#define spin_lock(lock) _spin_lock(lock)
#define _spin_lock(lock) __LOCK(lock)
#define __LOCK(lock)
do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)
注意到“preempt_disable()”,這個(gè)調(diào)用的功能是“關(guān)搶占”(在spin_unlock中會(huì)重新開(kāi)啟搶占功能)。從中可以看出,使用自旋鎖保護(hù)的區(qū)域是工作在非搶占的狀態(tài);即使獲取不到鎖,在“自旋”狀態(tài)也是禁止搶占的。了解到這,我想咱們應(yīng)該能夠理解為何自旋鎖保護(hù) 的代碼不能睡眠了。試想一下,如果在自旋鎖保護(hù)的代碼中間睡眠,此時(shí)發(fā)生進(jìn)程調(diào)度,則可能另外一個(gè)進(jìn)程會(huì)再次調(diào)用spinlock保護(hù)的這段代碼。而我們 現(xiàn)在知道了即使在獲取不到鎖的“自旋”狀態(tài),也是禁止搶占的,而“自旋”又是動(dòng)態(tài)的,不會(huì)再睡眠了,也就是說(shuō)在這個(gè)處理器上不會(huì)再有進(jìn)程調(diào)度發(fā)生了,那么 死鎖自然就發(fā)生了。
總結(jié)下自旋鎖的特點(diǎn):
- 單CPU非搶占內(nèi)核下:自旋鎖會(huì)在編譯時(shí)被忽略(因?yàn)閱蜟PU且非搶占模式情況下,不可能發(fā)生進(jìn)程切換,時(shí)鐘只有一個(gè)進(jìn)程處于臨界區(qū)(自旋鎖實(shí)際沒(méi)什么用了)
- 單CPU搶占內(nèi)核下:自選鎖僅僅當(dāng)作一個(gè)設(shè)置搶占的開(kāi)關(guān)(因?yàn)閱蜟PU不可能有并發(fā)訪問(wèn)臨界區(qū)的情況,禁止搶占就可以保證臨街區(qū)唯一被擁有)
- 多CPU下:此時(shí)才能完全發(fā)揮自旋鎖的作用,自旋鎖在內(nèi)核中主要用來(lái)防止多處理器中并發(fā)訪問(wèn)臨界區(qū),防止內(nèi)核搶占造成的競(jìng)爭(zhēng)。
四:linux發(fā)生搶占的時(shí)間
linux搶占發(fā)生的時(shí)間,搶占分為 用戶搶占和 內(nèi)核搶占。
用戶搶占在以下情況下產(chǎn)生:
- 從系統(tǒng)調(diào)用返回用戶空間
- 從中斷處理程序返回用戶空間
內(nèi)核搶占會(huì)發(fā)生在:
- 當(dāng)從中斷處理程序返回內(nèi)核空間的時(shí)候,且當(dāng)時(shí)內(nèi)核具有可搶占性
- 當(dāng)內(nèi)核代碼再一次具有可搶占性的時(shí)候(如:spin_unlock時(shí))
- 如果內(nèi)核中的任務(wù)顯示的調(diào)用schedule() (這個(gè)我暫時(shí)不太懂)
基本的進(jìn)程調(diào)度就是發(fā)生在時(shí)鐘中斷后,并且發(fā)現(xiàn)進(jìn)程的時(shí)間片已經(jīng)使用完了,則發(fā)生進(jìn)程搶占。通常我們會(huì)利用中斷處理程序返回內(nèi)核空間的時(shí)候可進(jìn)行內(nèi)核搶占這個(gè)特性來(lái)提高一些I/O操作的實(shí)時(shí)性,如:當(dāng)I/O事件發(fā)生的時(shí)候,對(duì)應(yīng)的中斷處理程序被激活,當(dāng)它發(fā)現(xiàn)有進(jìn)程在等待這個(gè)I/O事件的時(shí)候,它 會(huì)激活等待進(jìn)程,并且設(shè)置當(dāng)前正在執(zhí)行進(jìn)程的need_resched標(biāo)志,這樣在中斷處理程序返回的時(shí)候,調(diào)度程序被激活,原來(lái)在等待I/O事件的進(jìn)程 (很可能)獲得執(zhí)行權(quán),從而保證了對(duì)I/O事件的相對(duì)快速響應(yīng)(毫秒級(jí))??梢钥闯?,在I/O事件發(fā)生的時(shí)候,I/O事件的處理進(jìn)程會(huì)搶占當(dāng)前進(jìn)程,系統(tǒng) 的響應(yīng)速度與調(diào)度時(shí)間片的長(zhǎng)度無(wú)關(guān)。
五:spin_lock和mutex實(shí)際效率對(duì)比
1.++i是否需要加鎖?
我分別使用POSIX的spin_lock和mutex寫(xiě)了兩個(gè)累加的程序,啟動(dòng)了兩個(gè)線程,并利用時(shí)間戳計(jì)算它們執(zhí)行完累加所用的時(shí)間。
下面這個(gè)是使用spin_lock的代碼,我啟動(dòng)兩個(gè)線程同時(shí)對(duì)num進(jìn)行++,使用spin_lock保護(hù)臨界區(qū),實(shí)際上可能會(huì)有疑問(wèn)++i(++i和++num本文中是一個(gè)意思)為什么還要加鎖?
i++需要加鎖是很明顯的事情,對(duì)i++的操作的印象是,它一般是三步曲,從內(nèi)存中取出i放入寄存器中,在寄存器中對(duì)i執(zhí)行inc操作,然后把i放回內(nèi)存中。這三步明顯是可打斷的,所以需要加鎖。
但是++i可能就有點(diǎn)猶豫了。實(shí)際上印象流是不行的,來(lái)看一下i++和++i的匯編代碼,其實(shí)他們是一樣的,都是三步,我只上一個(gè)圖就行了,如下:
所以++i也不是原子操作,在多核的機(jī)器上,多個(gè)線程在讀取內(nèi)存中的i時(shí),可能讀取到同一個(gè)值,這就導(dǎo)致多個(gè)線程同時(shí)執(zhí)行+1,但實(shí)際上它們得到的結(jié)果是一樣的,即i只加了一次。還有一點(diǎn):這幾句匯編正說(shuō)明了++i和i++i對(duì)于效率是一樣的,不過(guò)這只是針對(duì)內(nèi)建POD類型而言,如果是class的話,我們都寫(xiě)過(guò)類的++運(yùn)算符的重載,如果一個(gè)類在單個(gè)語(yǔ)句中不寫(xiě)++i,而是寫(xiě)i++的話,那無(wú)疑效率會(huì)有很大的損耗。(有點(diǎn)跑題)
2.spin_lock代碼
首先是spin_lock實(shí)現(xiàn)兩個(gè)線程同時(shí)加一個(gè)數(shù),每個(gè)線程均++num,然后計(jì)算花費(fèi)的時(shí)間。
#include < iostream >
#include < thread >
#include < pthread.h >
#include < sys/time.h >
#include < unistd.h >
int num = 0;
pthread_spinlock_t spin_lock;
int64_t get_current_timestamp()
{
struct timeval now = {0, 0};
gettimeofday(&now, NULL);
return now.tv_sec * 1000 * 1000 + now.tv_usec;
}
void thread_proc()
{
for(int i=0; i< 100000000; ++i){
pthread_spin_lock(&spin_lock);
++num;
pthread_spin_unlock(&spin_lock);
}
}
int main()
{
pthread_spin_init(&spin_lock, PTHREAD_PROCESS_PRIVATE);//maybe PHREAD_PROCESS_PRIVATE or PTHREAD_PROCESS_SHARED
int64_t start = get_current_timestamp();
std::thread t1(thread_proc), t2(thread_proc);
t1.join();
t2.join();
std::cout< "num:"<
3.mutex代碼
#include < iostream >
#include < thread >
#include < pthread.h >
#include < sys/time.h >
#include < unistd.h >
int num = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int64_t get_current_timestamp()
{
struct timeval now = {0, 0};
gettimeofday(&now, NULL);
return now.tv_sec * 1000 * 1000 + now.tv_usec;
}
void thread_proc()
{
for(int i=0; i< 1000000; ++i){
pthread_mutex_lock(&mutex);
++num;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
int64_t start = get_current_timestamp();
std::thread t1(thread_proc), t2(thread_proc);
t1.join();
t2.join();
std::cout< "num:"<
4.結(jié)果分析
得出的結(jié)果如圖,num是最終結(jié)果,cost是花費(fèi)時(shí)間,單位為us,main2是使用spin lock,
顯然,在臨界區(qū)只有++num這一條語(yǔ)句的情況下,spin lock相對(duì)花費(fèi)的時(shí)間短一些,實(shí)際上它們有可能接近的情況,取決于CPU的調(diào)度情況,但始終會(huì)是spin lock執(zhí)行的效率在本情況中花費(fèi)時(shí)間更少。
我修改了兩個(gè)程序中臨界區(qū)的代碼,改為:
for(int i=0; i< 1000000; ++i){
pthread_spin_lock(&spin_lock);
++num;
for(int i=0; i< 100; ++i){
//do nothing
}
pthread_spin_unlock(&spin_lock);
}
另一個(gè)使用mutex的程序也加了這么一段,然后結(jié)果就與之前的情況大相徑庭了:
實(shí)驗(yàn)結(jié)果是如此的明顯,僅僅是在臨界區(qū)內(nèi)加了一個(gè)10圈的循環(huán),spin lock就需要花費(fèi)比mutex更長(zhǎng)的時(shí)間了。
所以, spin lock雖然lock/unlock的性能更好(花費(fèi)很少的CPU指令),但是它只適應(yīng)于臨界區(qū)運(yùn)行時(shí)間很短的場(chǎng)景。實(shí)際開(kāi)發(fā)中,程序員如果對(duì)自己程序的鎖行為不是很了解,否則使用spin lock不是一個(gè)好主意。更保險(xiǎn)的方法是使用mutex,如果對(duì)性能有進(jìn)一步的要求,那么再考慮spin lock。
六:使用C++實(shí)現(xiàn)自主實(shí)現(xiàn)自旋鎖
由于前面原理已經(jīng)很清楚了,現(xiàn)在直接給代碼如下:
#pragma once
#include < atomic >
class spin_lock {
private:
std::atomic< bool > flag = ATOMIC_VAR_INIT(false);
public:
spin_lock() = default;
spin_lock(const spin_lock&) = delete;
spin_lock& operator=(const spin_lock) = delete;
void lock(){ //acquire spin lock
bool expected = false;
while(!flag.compare_exchange_strong(expected, true));
expected = false;
}
void unlock(){ //release spin lock
flag.store(false);
}
};
測(cè)試文件,僅給出關(guān)鍵部分:
int num = 0;
spin_lock sm;
void thread_proc()
{
for(int i=0; i< 10000000; ++i){
sm.lock();
++num;
sm.unlock();
}
}
好的,對(duì)自旋鎖的總結(jié)就先到這里了。
-
多線程
+關(guān)注
關(guān)注
0文章
277瀏覽量
19897 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4277瀏覽量
62323 -
C++
+關(guān)注
關(guān)注
21文章
2100瀏覽量
73453 -
自旋鎖
+關(guān)注
關(guān)注
0文章
11瀏覽量
1574
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論