Linux多執行緒學習(4) --讀寫鎖和其他型別的鎖以及執行緒安全
多執行緒學習總結(1):https://blog.csdn.net/hansionz/article/details/84665815
多執行緒學習總結(2):https://blog.csdn.net/hansionz/article/details/84675536
多執行緒學習總結(3):https://blog.csdn.net/hansionz/article/details/84766601
Linux多執行緒學習
一.讀寫鎖
1.什麼是讀寫鎖
學習多執行緒
的時候,有一種情況是十分常見的。那就是有些公共資料修改
的機會比較少。相比較改寫,它們讀的機會反而高
的多。通常而言,對臨界資源
的修改,一定要加上互斥量去保護臨界資源
,但是在讀的過程
中,往往伴隨著查詢
的操作,中間耗時
程式碼段加互斥鎖
,會極大地降低程式的效率
。讀寫鎖就是解決多讀少寫
問題。
讀寫鎖支援當沒有執行緒去寫入
的時候,可以存在多個讀者執行緒同時去共享的訪問臨界資源
,而當臨界區沒有讀者執行緒去訪問或者沒有寫者執行緒去寫的時候
才允許該執行緒去寫
。這種用於共享訪問給定資源的讀寫鎖
,也叫共享-獨佔鎖
,獲取一個讀寫鎖用於讀稱為共享鎖
,獲取一個讀寫鎖用於寫稱為獨佔鎖
。
2.讀者和寫者的關係
- 讀者和讀者:
共享
關係。可以允許多個執行緒同時讀 - 寫者和寫者:
互斥
關係。當有一個執行緒在寫,其他執行緒不能寫入 - 讀者和寫者:
步與互斥
。當有讀者在讀或者寫者在寫的時候,不能存在其他執行緒寫;噹噹有執行緒在讀的時候,不能有其他執行緒寫入。
讀寫鎖分配規則:
- 只要沒有執行緒拿著
讀寫鎖用於寫
,任意數目
的執行緒可以拿到讀寫鎖用來讀
- 如果沒有執行緒拿著讀寫鎖用來
讀或寫
的時候,才可以存線上程用來寫
讀寫鎖的行為:
總結:寫獨佔、讀共享、寫鎖優先順序高
3.初始化讀寫鎖
讀寫鎖的資料型別為pthread_rwlock_t
。它存在兩種初始化方式:
- 靜態方法:
//直接給讀寫鎖變數賦值PTHREAD_RWLOCK_INITALIZER
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITALIZER
- 動態分配
//呼叫函式動態初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
引數:
rwlock為讀寫鎖變數的地址
attr為讀寫鎖的屬性,一般不使用可以設定為NULL
返回值:成功返回0,失敗返回錯誤碼
4.銷燬讀寫鎖
當所有的執行緒不在持
有也不在去申請讀寫鎖
的時候,該讀寫鎖應該被銷燬
。
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
引數:
rwlock為讀寫鎖變數的地址
返回值:成功返回0,失敗返回錯誤碼
5.獲取和釋放讀寫鎖
pthread_rwlock_rlock
函式用來獲取一個讀鎖,如果對於的讀寫鎖
被某個寫者執行緒擁有,則該函式會阻塞呼叫執行緒
。pthread_rwlock_wrlock
用來獲取一個寫鎖,如果對應的讀寫鎖由另一個寫入者
或者一個或多個讀者
所有,那麼阻塞呼叫執行緒。pthread_rwlock_unlock
函式用來釋放一個讀鎖或寫鎖
。
//獲取讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//獲取寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//釋放讀寫鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,出錯返回錯誤碼
下面兩個函式用來嘗試獲取讀寫鎖
,但是如果鎖不能立即獲得
,就返回EBUSY
錯誤,而不是呼叫阻塞
執行緒。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
返回值:成功返回0,出錯返回錯誤碼
注:以上的函式均位於標頭檔案<pthread.h>
中
6.讀寫鎖的例項
#include <iostream>
#include <unistd.h>
using namespace std;
int book = 0;
pthread_rwlock_t rwlock;
//可以存在任意多個執行緒同時讀
void *read_routine(void *arg)
{
while(1)
{
pthread_rwlock_rdlock(&rwlock);
cout << "my tid is:" << pthread_self() << ".read book data is:" << book << endl;
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
//任意時刻只能有一個寫者執行緒在寫(在寫的時候不能有讀者讀)
void *write_routine(void *arg)
{
while(1)
{
pthread_rwlock_wrlock(&rwlock);
++book;
cout << "my tid is:"<< pthread_self() << ".write book data is:" << book << endl;
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
}
int main()
{
//初始化讀寫鎖變數
pthread_rwlock_init(&rwlock, NULL);
pthread_t r1,r2,w1,w2;//建立兩個讀者和寫者執行緒
pthread_create(&r1, NULL, read_routine, NULL);
pthread_create(&r2, NULL, read_routine, NULL);
pthread_create(&w1, NULL, write_routine, NULL);
pthread_create(&w2, NULL, write_routine, NULL);
pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(w1, NULL);
pthread_join(w2, NULL);
//銷燬讀寫鎖
pthread_rwlock_destroy(&rwlock);
return 0;
}
以上程式的執行結果應該是任意一個時刻,只存在一個寫者在修改book
變數,但是當沒有寫者在寫的時候,可以允許兩個讀者同時在讀book
變數的值。以下為執行結果:
二.其他常見的各種鎖
1.樂觀鎖和悲觀鎖
- 悲觀鎖::在每次取資料時,總是
擔心資料
會被其他執行緒修改,所以會在取資料前先加鎖(讀鎖,寫鎖, 行鎖等)
,當其他執行緒想要訪問資料時,被阻塞
掛起。悲觀鎖適用於多寫的場景,因為多寫的情況會經常產生衝突。 - 樂觀鎖:每次取資料時候,總是
樂觀
的認為資料不會被其他執行緒
修改,因此不上鎖
。但是在更新資料前
,會判斷
其他執行緒在更新前有沒有
對資料進行修改。樂觀鎖適用於多讀
的應用型別,這樣可以提高吞吐量
。主要採用兩種方式:版本號機制和CAS操作。 - CAS操作:當需要
更新資料
時,判斷當前記憶體值
和之前取得的值
是否相等。如果相等則用新值更新
。若不等則失敗
,失敗則重試。 即compare and swap(比較與交換)
,是一種有名的無鎖演算法
。無鎖程式設計,即不使用鎖的情況下實現多執行緒
之間的變數同步
,也就是在沒有執行緒被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
。CAS演算法涉及到三個運算元:需要讀寫的記憶體值 V
、進行比較的值 A
、擬寫入的新值 B
。當且僅當 V 的值等於A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。 - 版本號機制:一般是在
資料表
中加上一個資料版本號version
欄位,表示資料被修改的次數
,當資料被修改時,version
值會加1。當執行緒A要更新資料
值時,在讀取資料的同時也會讀取version
值,在提交更新
時,若剛才讀取到的version
值為當前資料庫中的version
值相等時才更新,否則重試
更新操作,直到更新成功。
推薦閱讀:https://blog.csdn.net/qq_34337272/article/details/81072874
2.自旋鎖
自旋鎖(spinlock)
:是指當一個執行緒在獲取鎖
的時候,如果鎖已經被其它執行緒獲取
,那麼該執行緒將迴圈等待
,然後不斷的判斷鎖
是否能夠被成功獲取,直到獲取到鎖才會退出迴圈
。
獲取鎖的執行緒一直處於活躍狀態
,但是並沒有執行任何有效的任務
,使用這種鎖會造成busy-waiting
。
它是為實現保護共享資源
而提出一種鎖機制
。其實,自旋鎖
與互斥鎖
比較類似,它們都是為了解決對某項資源
的互斥
使用。無論是互斥鎖
,還是自旋鎖
,在任何時刻,最多隻能有一個保持者
,也就說,在任何時刻最多隻能有一個執行單元
獲得鎖。但是兩者在排程機制
上略有不同。對於互斥鎖
,如果資源已經被佔用,資源申請者只能進入睡眠狀態
。但是自旋鎖
不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持
,呼叫者就一直迴圈在那裡
看是否該自旋鎖的保持者已經釋放
了鎖,”自旋”
一詞就是因此而得名。
推薦閱讀:https://blog.csdn.net/qq_34337272/article/details/81252853
3.公平鎖和非公平鎖
- 公平鎖:按照執行緒
加鎖的順序
來分配,即先來先得FIFO
- 非公平鎖:一種
獲取鎖
的搶佔機制,是隨機的獲得鎖
的,這樣可能會有些執行緒一直會拿不到鎖
,結果也就是不公平
的
三.執行緒安全
1.什麼是執行緒安全
執行緒安全是多執行緒程式設計
中的一個概念。在擁有共享資料
的多個執行緒
並行執行的程式中,執行緒安全
的程式碼會通過同步機制
保證各個執行緒都可以正常且正確
的執行,不會出現資料汙染
等錯誤情況。
2.執行緒安全版本的單例模式
對於執行緒安全版本的單例模式單獨總結於我的另外一邊部落格:https://blog.csdn.net/hansionz/article/details/83752531
主要問題:
- 加鎖解鎖的
位置
- 雙重
if
判定, 避免不必要的鎖競爭 volatile
關鍵字防止過度優化
3.STL中的容器是否是執行緒安全的
答:不是
。原因是STL
的設計初衷是將效能挖掘到極致
, 而一旦涉及到加鎖保證
執行緒安全, 會對效能
造成巨大的影響。且對於不同的容器
加鎖方式的不同,效能可能也不同(例如hash表的鎖表和鎖桶)
。 因此 STL
預設不是執行緒安全, 如果需要在多執行緒環境
下使用,往往需要呼叫者自行保證
執行緒安全。
4.智慧指標是否是執行緒安全的
- 對於
unique_ptr
, 由於只是在當前程式碼塊範圍
內生效, 因此不涉及執行緒安全
問題。 - 對於
shared_ptr
, 多個物件需要共用一個引用計數變數
, 所以會存在執行緒安全
問題.。但是標準庫實現的時候考慮到了這個問題
,基於原子操作(CAS)
的方式保證shared_ptr
能夠高效,原子的操作引用計數。