鎖是一個常見的同步概念,我們都聽說過加鎖(lock)或者解鎖(unlock),當然學術一點的說法是獲取(acquire)和釋放(release)。
恰好pthread包含這幾種鎖的API,而C++11只包含其中的部分。接下來我主要通過pthread的API來展開本文。
mutex(互斥量)
mutex(mutual exclusive)即互斥量(互斥體)。也便是常說的互斥鎖。
盡管名稱不含lock,但是稱之為鎖,也是沒有太大問題的。mutex無疑是最常見的多線程同步方式。其思想簡單粗暴,多線程共享一個互斥量,然后線程之間去競爭。得到鎖的線程可以進入臨界區執行代碼。
//聲明一個互斥量
pthread_mutex_tmtx;
//初始化
pthread_mutex_init(&mtx,NULL);
//加鎖
pthread_mutex_lock(&mtx);
//解鎖
pthread_mutex_unlock(&mtx);
//銷毀
pthread_mutex_destroy(&mtx);
mutex是睡眠等待(sleep waiting)類型的鎖,當線程搶互斥鎖失敗的時候,線程會陷入休眠。優點就是節省CPU資源,缺點就是休眠喚醒會消耗一點時間。另外自從Linux 2.6版以后,mutex完全用futex的API實現了,內部系統調用的開銷大大減小。
值得一提的是,pthread的鎖一般都有一個trylock的函數,比如對于互斥量:
ret=pthread_mutex_trylock(&mtx);
if(0==ret){//加鎖成功
...
pthread_mutex_unlock(&mtx);
}elseif(EBUSY==ret){//鎖正在被使用;
...
}
pthread_mutex_trylock用于以非阻塞的模式來請求互斥量。就好比各種IO函數都有一個noblock的模式一樣,對于加鎖這件事也有類似的非阻塞模式。
當線程嘗試加鎖時,如果鎖已經被其他線程鎖定,該線程就會阻塞住,直到能成功acquire。但有時候我們不希望這樣。
pthread_mutex_trylock在被其他線程鎖定時,會返回特殊錯誤碼。加鎖成返回0,僅當成功但時候,我們才能解鎖在后面進行解鎖操作!
C++11開始引入了多線程庫
此外,依據同一線程是否能多次加鎖,把互斥量又分為如下兩類:
- 是:稱為『遞歸互斥量』recursive mutex ,也稱『可重入鎖』reentrant lock
- 否:即『非遞歸互斥量』non-recursive mute),也稱『不可重入鎖』non-reentrant mutex
若同一線程對非遞歸的互斥量多次加鎖,可能會造成死鎖。遞歸互斥量則無此風險。C++11中有遞歸互斥量的API:std::recursive_mutex。對于pthread則可以通過給mutex添加PTHREAD_MUTEX_RECURSIVE 屬性的方式來使用遞歸互斥量:
//聲明一個互斥量
pthread_mutex_tmtx;
//聲明一個互斥量的屬性變量
pthread_mutexattr_tmtx_attr;
//初始化互斥量的屬性變量
pthread_mutexattr_init(&mtx_attr);
//設置遞歸互斥量的屬性
pthread_mutexattr_settype(&mtx_attr,PTHREAD_MUTEX_RECURSIVE);
//把屬性賦值給互斥量
pthread_mutext_init(&mtx,&mutext_attr);
然而對于遞歸互斥量或者說可重入鎖的使用則需要克制。Stevens大神生前在《APUE》中說『使用好它是十分tricky的,僅當沒有其他解決方案時才使用』。
可重入鎖這個概念和稱呼的走俏多半是Java語言的功勞。
condition variable(條件變量)
請注意條件變量不是鎖,它是一種線程間的通訊機制,并且幾乎總是和互斥量一起使用的。所以互斥量和條件變量二者一般是成套出現的。比如C++11中也有條件變量的API:std::condition_variable。
對于pthread:
//聲明一個互斥量
pthread_mutex_tmtx;
//聲明一個條件變量
pthread_cond_tcond;
...
//初始化
pthread_mutex_init(&mtx,NULL);
pthread_cond_init(&cond,NULL);
//加鎖
pthread_mutex_lock(&mtx);
//加鎖成功,等待條件變量觸發
pthread_cond_wait(&cond,&mtx);
...
//加鎖
pthread_mutex_lock(&mtx);
pthread_cond_signal(&cond);
...
//解鎖
pthread_mutex_unlock(&mtx);
//銷毀
pthread_mutex_destroy(&mtx)
pthread_cond_wait函數會把條件變量和互斥量都傳入。并且多線程調用的時候條件變量和互斥量一定要一一對應,不能一個條件變量在不同線程中wait的時候傳入不同的互斥量。否則是未定義結果。
關于是先解鎖互斥量還是先進行條件變量的通知,是另外一個比較大的議題。有種論斷說:先解鎖互斥量再通知條件變量可以減少多余的上下文切換,進而提高效率。這種說法是基于一種實現假設:先通知條件變量,再解鎖。
可能讓其他等待條件變量的線程被喚醒了,但是此時互斥量還沒解鎖,從而再次陷入休眠。然而對于另外一些實現,比如Linux系統,則通過等待變形(wait morphing)解決了這一問題。所以先通知再解鎖也沒用問題。
另外在使用條件變量的過程中有個稍微違反直覺的寫法:那就是使用while而不是if來做判斷狀態是否滿足。這樣做的原因有二:
- 避免驚群;
- 避免某些情況下線程被虛假喚醒(即沒有pthread_cond_signal就解除了阻塞)。
比如半同步/半reactor的網絡模型中,在工作線程消費fd隊列的時候:
while(1){
if(pthread_mutex_lock(&mtx)!=0){//加鎖
...//異常邏輯
}
while(!queue.empty()){
if(pthread_cond_wait(&cond,&mtx)!=0){
...//異常邏輯
}
}
autodata=queue.pop();
if(pthread_mutex_unlock(&mtx)!=0){//解鎖
...//異常邏輯
}
process(data);//處理流程,業務邏輯
}
read-write lock(讀寫鎖)
顧名思義『讀寫鎖』就是對于臨界區區分讀和寫。在讀多寫少的場景下,不加區分的使用互斥量顯然是有點浪費的。此時便該上演讀寫鎖的拿手好戲。
讀寫鎖有一個別稱叫『共享-獨占鎖』。不過單看『共享-獨占鎖』或者『讀寫鎖』這兩個名稱,其實并未區分對于讀和寫,到底誰共享,誰獨占。可能會讓人誤以為讀寫鎖是一種更為泛化的稱呼,其實不是。讀寫鎖的含義是準確的:是一種 讀共享,寫獨占的鎖。
讀寫鎖的特性:
- 當讀寫鎖被加了寫鎖時,其他線程對該鎖加讀鎖或者寫鎖都會阻塞(不是失敗)。
- 當讀寫鎖被加了讀鎖時,其他線程對該鎖加寫鎖會阻塞,加讀鎖會成功。
因而適用于多讀少寫的場景。
//聲明一個讀寫鎖
pthread_rwlock_trwlock;
...
//在讀之前加讀鎖
pthread_rwlock_rdlock(&rwlock);
...共享資源的讀操作
//讀完釋放鎖
pthread_rwlock_unlock(&rwlock);
//在寫之前加寫鎖
pthread_rwlock_wrlock(&rwlock);
...共享資源的寫操作
//寫完釋放鎖
pthread_rwlock_unlock(&rwlock);
//銷毀讀寫鎖
pthread_rwlock_destroy(&rwlock);
其實加讀鎖和加寫鎖這兩個說法可能會造成誤導,讓人誤以為是有兩把鎖,其實讀寫鎖是一個鎖。所謂加讀鎖和加寫鎖,準確的說法可能是『給讀寫鎖加讀模式的鎖定和加寫模式的鎖定』。
讀寫鎖和互斥量一樣也有trylock函數,也是以非阻塞地形式來請求鎖,不會導致阻塞。
pthread_rwlock_tryrdlock(&rwlock)
pthread_rwlock_trywrlock(&rwlock)
C++11中有互斥量、條件變量但是并沒有引入讀寫鎖。而在C++17中出現了一種新鎖:std::shared_mutex。用它可以模擬實現出讀寫鎖。demo代碼可以直接參考cppreference:
https://en.cppreference.com/w/cpp/thread/shared_mutex
另外多讀少寫的場景有些特殊場景,可以用特殊的數據結構減少鎖使用:
- 多讀單寫的線性數據。用數組實現環形隊列,避免vector等動態擴張的數據結構,寫在結尾,由于單寫因而可以不加鎖;讀在開頭,由于多讀(避免重復消費)所以需要加一下鎖(互斥量就行)。
- 多讀單寫的KV。可以使用雙緩沖(double buffer)的數據結構來實現。double buffer同名的概念比較多,這里指的是foreground 和 backgroud 兩個buffer進行切換的『0 - 1切換』技術。比如實現動態加載(熱加載)配置文件的時候。可能會在切換間隙加一個短暫的互斥量,但是基本可以認為是lock free的。
我一張口,你就會發現:無非是空間換時間的老套路了。
spinlock(自旋鎖)
自旋之名頗為玄妙,第一次聽聞常讓人略覺高大。但和無數個好似『故意把簡單概念復雜化』的計算機術語一樣,自旋鎖的本質簡單的難以置信。
要了解自旋鎖,首先了解自旋。什么是自旋(spin)呢?更為通俗的一個詞是『忙等待』(busy waiting)。最最通俗的一個理解,其實就是死循環……。
單看使用方法和使用互斥量的代碼是差不多的。只不過自旋鎖不會引起線程休眠。當共享資源的狀態不滿足的時候,自旋鎖會不停地循環檢測狀態。因為不會陷入休眠,而是忙等待的方式也就不需要條件變量。
這是優點也是缺點。不休眠就不會引起上下文切換,但是會比較浪費CPU。
//聲明一個自旋鎖變量
pthread_spinlock_tspinlock;
//初始化
pthread_spin_init(&spinlock,0);
//加鎖
pthread_spin_lock(&spinlock);
//解鎖
pthread_spin_unlock(&spinlock);
//銷毀
pthread_spin_destroy(&spinlock);
pthread_spin_init函數的第二個參數名為pshared(int類型)。表示的是是否能進程間共享自旋鎖。這被稱之為Thread Process-Shared Synchronization。互斥量的通過屬性也可以把互斥量設置成進程間共享的。pshared有兩個枚舉值:
- PTHREAD_PROCESS_PRIVATE:僅同進程下讀線程可以使用該自旋鎖
- PTHREAD_PROCESS_SHARED:不同進程下的線程可以使用該自旋鎖
在Linux上的glibc中這兩個枚舉值分別是0和1(Mac上不是)。所以通常也會看到直接傳0的代碼。你可能覺得不使用宏,直接用數字硬編碼不是一個好習慣。的確,妥妥的Magic Number,但還有一個有趣的事實你需要了解:并不是所有實現都支持自旋鎖設置pshared。比如:
intpthread_spin_init(pthread_spinlock_t*lock,intpshared){
/*RelaxedMOisfinebecausethisisaninitializingstore.*/
atomic_store_relaxed(lock,0);
return0;
}
所以直接傳0可能也無傷大雅。
自旋鎖 VS 互斥量+條件變量 孰優孰劣?肯定要看具體的使用場景,(我好像在說片湯話)。當你不知道在你的使用場景下這兩種鎖該用哪個的時候,那就是用互斥量吧!
或者通過壓測的判斷,不過大多數時候我們好像并不需要這么一個pthread的自旋鎖,知友們可以提供一些自旋鎖的使用參考。
-
cpu
+關注
關注
68文章
10868瀏覽量
211844 -
數據
+關注
關注
8文章
7045瀏覽量
89062 -
Mac
+關注
關注
0文章
1106瀏覽量
51485 -
函數
+關注
關注
3文章
4332瀏覽量
62641
原文標題:如何理解互斥鎖、條件變量、讀寫鎖以及自旋鎖?
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論