1. 程式人生 > >Linux多執行緒學習(4) --讀寫鎖和其他型別的鎖以及執行緒安全

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能夠高效,原子的操作引用計數。