文章目錄
簡 述: 上一篇中介紹了多線程使用互斥量(鎖)來控制程序的訪問公共資源的時候是”串行“的;本篇繼續,重點講解如下幾個概念:Linux 中的原子操作 、死鎖原因 以及解決方法 、和讀寫鎖 和對應的源碼小例子。其中讀寫鎖的使用例子,完全可以參考互斥量(鎖),其大概流程如下:
pthread_rwlock_init()
pthread_rwlock_rdlock()
/pthread_rwlock_tryrdlock()
/pthread_rwlock_wrlock()
/pthread_rwlock_trywrlock()
- 、、、代碼片
pthread_rwlock_unlock()
pthread_rwlock_destroy()
編程環境:
💻: uos20
📎 gcc/g++ 8.3
📎 gdb8.0
💻: MacOS 10.14
📎 gcc/g++ 9.2
📎 gdb8.3
原子操作:
-
原子操作:
- cpu 處理一個指令,線程 / 進程在處理完這個指令之前,是不會失去 cpu 的。
借用顯示生活中的知識,原子⚛是最小的不可分割的物質,沒有比它更小的(類比,不詳探究夸克);在一個程序中,是有幾百行代碼構成了,可以將一行代碼(一行表達式語句)看做爲一個 ”原子操作“;
比如:
printf("");
int a = b + 100;
-
臨界區:
- 從代碼的角度理解,就是 執行加鎖語句
pthread_mutex_lock()
和解鎖語句pthread_mutex_unlock()
之間代碼片,稱之爲 臨界區; 也可以看作爲 ”僞原子操作“ ,因爲它有可能臨界區的代碼執行到一半,cpu 就被搶走了,但是其雖然搶到了 cpu 但是會阻塞,或者不能夠訪問該臨界區的代碼片,然後等待輪轉,cpu 再次回來,繼續在自己身上繼續執行接下來的代碼行;然後這樣臨界區的代碼就只有它執行完畢了。可以看做是一個 ”僞“ 原子操作。
示意圖如下:
- 從代碼的角度理解,就是 執行加鎖語句
造成死鎖的原因:
自己鎖自己:
- 分析: 當遇到連續鎖兩次的時候,線程會阻塞在 第二個
pthread_mutex_lock()
函數這一行裏面。
循環鎖住:
避免死鎖的方式:
避免或者解決死鎖的三種方式如下:
- 讓線程按照一定的順序訪問共享資源
- 在訪問其他鎖的時候,需要先將自己的鎖解開
- 設置上鎖的使用,可以使用
pthread_mutex_trylock()
函數
讀寫鎖:
除了使用互斥量(鎖)之外,還可以採用 讀寫鎖 來控制多線程訪問共享資源。
讀寫鎖的理解:
- 讀鎖 - 對內存做讀操作
- 寫鎖 - 對內存做寫操作
讀寫鎖的特性:
- 線程 A 加鎖成功,又來了三個線程,做讀操作,可以加鎖成功
- 讀共享 - 並行處理
- 線程 A 加寫鎖成功,又來了三個線程,做讀操作,三個線程阻塞
- 寫獨佔
- 線程 A 加讀鎖成功,又來了 B 線程加寫鎖線程阻塞,又來了 C 線程加讀鎖阻塞
- 讀寫不能同時進行
- 寫的優先級高(即使後面線程有先後來順序來,也會看一下優先級)
讀寫鎖的場景練習:
上面的讀寫鎖的特性 可以看做是理論部分,然後這裏用幾個實際場景進行一下分析:
- 線程 A 加寫鎖成功,線程 B 請求讀鎖
- 線程 B 阻塞
- 線程 A 持有讀鎖,線程 B 請求寫鎖
- 線程 B 阻塞
- 線程 A 擁有讀鎖,線程 B 請求讀鎖
- 線程 B 加鎖成功
- 線程 A 持有讀鎖,然後線程 B 請求寫鎖,然後線程 C 請求讀鎖
- B 阻塞,C 阻塞 -寫的優先級高
- A 解鎖,B 線程加寫鎖成功,C繼續阻塞
- B 解鎖,C 加讀鎖成功
- 線程A持有寫鎖,然後線程B請求讀鎖,然後線程C請求寫鎖
讀寫鎖的使用場景:
- 互斥鎖 - 讀寫串行
- 讀寫鎖:
- 讀:並行
- 寫:串行
- 程序中的 “讀操作” 大於 ”寫操作“ 的時候,比如說 12306 買火車票的例子 ,就有大量的率新讀取數據,遠大於買票的時候寫操作。
讀寫鎖的主要操作函數:
讀寫鎖的使用流程和互斥量(鎖)的流程基本一樣。
-
初始化讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *lock, const pthread_rwlockattr_t *attr);
-
銷燬讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *lock);
-
加讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *lock);
-
嘗試加讀鎖
int pthread_rwlock_tryrdlock(pthread_rwlock_t *lock);
-
加寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *lock);
-
嘗試加寫鎖
int pthread_rwlock_trywrlock(pthread_rwlock_t *lock);
-
解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *lock);
寫一個運用讀寫鎖的例子:
上面例子講了這麼多用法和屬性作爲鋪墊,這裏有一代碼例子,講解讀寫鎖的使用例子:
-
需求練習:
- 3 個線程不定時寫同一全局資源,5 個線程不定時讀同一全局資源
-
代碼:
#include <stdio.h> #include <unistd.h> #include <pthread.h> int g_number = 0; pthread_rwlock_t lock; void* writeFunc(void* arg); void* readFunc(void* arg); int main(int argc, char *argv[]) { pthread_t p[8]; pthread_rwlock_init(&lock, nullptr); //初始化一個鎖 for (int i = 0; i < 3; i++) { //創建寫線程 pthread_create(&p[i], nullptr, writeFunc, nullptr); } for (int i = 3; i < 8; i++) { //創建讀線程 pthread_create(&p[i], nullptr, readFunc, nullptr); } for (int i = 0; i < 8; i++) { //阻塞回收子線程的 pcb pthread_join(p[i], nullptr); } pthread_rwlock_destroy(&lock); //銷燬讀寫鎖,釋放鎖資源 return 0; } void* writeFunc(void* arg) { while (true) { pthread_rwlock_wrlock(&lock); //加寫鎖 g_number++; printf("--write: %lu, %d\n", pthread_self(), g_number); pthread_rwlock_unlock(&lock); //解鎖 usleep(500); } return nullptr; } void* readFunc(void* arg) { while (true) { pthread_rwlock_rdlock(&lock); //加讀鎖 printf("--read : %lu, %d\n", pthread_self(), g_number); pthread_rwlock_unlock(&lock); //解鎖 usleep(500); } return nullptr; }
-
運行效果:
-
屏蔽去掉加鎖解鎖的幾行註釋,則會出現以下異常情況,小數可能在大數後面再執行打印語句:
-
加上讀寫鎖之後,得到預期正確結果,大數只會出現在小數後面打印
-
下載地址:
歡迎 star 和 fork 這個系列的 linux 學習,附學習由淺入深的目錄。