在之前的部落格中,我講到線程的相關概念和線程的控制,在本節中我們聊一下線程互斥。
五個概念
-
臨界資源
多線程執行流共享的資源叫做臨界資源。大量執行流同時通路時可能會導緻資料二義性的問題。
-
臨界區
每個線程内部通路臨界資源區的代碼叫做臨界。我們可以通過控制代碼的讀寫規則保證臨界區的安全性。
-
互斥
任何時刻有且僅有一個執行流進入臨界區的情況稱互斥。我們通常在通路臨界資源時會對它進行加鎖保護,保證資料的安全性。
-
同步
在保證臨界資源安全的情況下讓多執行流按一定順序通路它稱同步。同步非同時,保證同步是為了協調多執行流的步調,避免饑餓問題。
-
原子性
不會被任何排程機制打斷的操作,該操作隻有兩種狀态,要麼完成,要麼未開始。一般隻需要一條彙編代碼就可以完成解析。
多線程并發的問題
大部分情況下,線程使用的資料局部變量,變量的位址空間線上程棧空間内,這種情況下變量隻屬于單個線程,其他線程無法通路。但是也有時候,我們的變量需要在程序間共享,這樣的變量稱為共享資料可用于完成線程間的互動。
然而多線程并發的操作共享變量會帶來一些問題:
先看下面代碼
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int goal=0;
void* handle(void* arg)
{
int count=10000000;
while(count--)
goal++;
return (void*)1;
}
int main()
{
pthread_t t1,t2;
pthread_create(&t1,NULL,handle,NULL);
pthread_create(&t2,NULL,handle,NULL);
sleep(1);
cout<<goal<<endl;
pthread_join(t1,NULL);
pthread_join(t2,NULL);
return 0;
}
不看不知道,一看吓一跳!!本來我以為我寫了兩個線程分别對goal加10000000次,最後的結果應該是20000000,然而!!

通過運作結果,我發現運作了三次的結果都不相同,而且也沒有正确的結果。那麼問題出在哪裡呢?
原來是因為兩個線程是并發運作的,不同的線程在執行時有不同的寄存器,他們可能同時取走了某一時刻的goal,對它分别進行++後結果都加了一,但最後寫回記憶體時由原來的加2變成隻加1,導緻結果越錯越離譜。這裡的根本原因是++這個操作并不是原子性的。看下圖你應該就能明白了
來看我們在vs2013下一段簡單的反彙編代碼:從圖中你可以清楚的看到對于goal的++操作分解成了三步,是以這就是導緻上面問題的罪魁禍首
如果++是一個原子性的操作的話,那麼就不會出現上述的問題,是以現在要想解決上面的問題要麼将上述代碼實作原子性操作,要麼實作互斥的機制,Linux中确實提供了原子的++操作(atomic)但是這僅僅解決了目前問題,是以我們這裡重點介紹線程的互斥鎖實作互斥。
互斥量mutex
為了解決上面的問題,我們需要做到3點:
- 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其他線程進入該臨界區
- 如果多個線程同時要執行臨界區的代碼,并且沒有線程在執行,那麼隻允許一個線程進入該臨界區
- 如果線程不在臨界區中執行,那麼該線程不能阻止其他線程進入臨界區
要做到以上三點,本質上需要一把鎖,Linux下提供了一把鎖叫做互斥量:
接下來我介紹一下互斥量的接口。
互斥量的接口
初始化互斥量:有兩種方法
靜态配置設定
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
動态配置設定
int pthread_mutex_init(pthread_mutex_t restrict mutex, const pthread_mutexattr_trestrict attr);
//參數1為要初始化的互斥鎖,參數2一般設定為NULL表示使用預設屬性
銷毀互斥量:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要銷毀
不要銷毀一個被加鎖的互斥量
已經銷毀的互斥量不能再被線程加鎖
互斥量加鎖和解鎖:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
注意:調用pthread_lock時,可能會遇到以下情況:
互斥量處于未鎖狀态,該函數會将互斥量鎖定,同時傳回成功
發起函數調用時,其他線程已經鎖定互斥量,或者存在其他線程同時申請互斥量,但沒有競争到互斥量,那麼pthread_ lock調用會陷入阻塞(執行流被挂起),等待互斥量解鎖
下面我們使用上面的函數,修改之前的代碼為線程安全的:
這時如果目前沒有線程在臨界區中,那麼線程一通路臨界資源的時候會先進行加鎖處理,當線程二要通路臨界資源時發現資源被鎖,是以OS将線程二設定為非r狀态并将其加入到阻塞隊列中等待線程一釋放鎖資源。這樣就可以保證線程在對臨界區通路時任一時刻隻有一個線程,也就是說我們現在可以認為我們的程式++是原子的。
運作結果如下:
互斥量實作的原理
互斥鎖的實作原理實際上非常的簡單,我之前說到過,當彙編是一句指令時就可以認為是原子的,是以為了實作鎖的互斥操作,大多數體系結構對于互斥鎖的實作使用了swap或者exchange指令,該指令的作用是把寄存器和單中繼資料交換,因為就算是多線程,總線周期也有先後時間,是以利用此指令總能保證同一個時間隻有一個線程進入臨界區。
現在我們來看看如何使用swap或者exchange指令實作互斥鎖原理,來看下面的一段僞代碼:可以看到鎖資源最開始拿到的值為1,然後使用swap或exchange指令将寄存器中的值和鎖擁有的值交換,如果寄存器中現在值為1,那麼表示資源申請成功,如果寄存器中值為0,說明鎖資源中的1已經被其他線程交換走了,是以目前程序也就需要進入等待隊列。
注意: 加鎖操作要求原子性,解鎖操作無要求。
線程安全
線程安全:多個線程并發處理同一段代碼時,不會出現不同的結果。常見的對全局變量或者靜态變量進行操作并且沒有鎖的保護下,會出現不同的結果。
重入:同一函數被不同的執行流調用,目前執行流未執行完畢就有其他執行流再次進入,稱這種現象為重入。一個函數在重入情況下,運作結果不會出現任何問題,稱為可重入函數,否則稱為不可重入函數。
常見線程不安全的情況:
- 不保護共享變量的函數
- 函數的狀态随着被調用,函數發生變化的函數
- 傳回指向靜态變量指針的函數
- 調用線程不安全函數的函數
常見線程安全的情況:
- 每個線程對于全局變量或者靜态變量隻有讀取的權限,而沒有寫入的權限
- 類或者接口對于線程來說都是原子操作
- 多個線程之間的切換不會導緻該接口的執行結果存在二義性
可重入與線程安全
- 可重入函數一定是線程安全的,線程安全的函數不一定可重入
- 不可重入函數不能由多個線程使用過,否則有線程安全問題
- 若一個函數中有全局變量,則這個函數既不是線程安全也不是可重入的