Linux C/C++多線程同步(互斥量,死鎖,讀寫鎖,條件變量,信號量,文件鎖)
1. 線程同步的一些概念
1.1 同步的概念
所謂同步,即同時起步,不同的對象,對“同步”的理解方式略有不同。如,設備同步,是指在兩個設備間規定一個共同的時間參考;數據同步,是指讓兩個或多個數據庫內容保持一致,或者按需要部分保持一致。文件同步,是指讓兩個或多個文件夾裏的內容保持一致,等等。
而編程中、通信中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協同、協助、互相配合。主旨在協同步調,按預定的先後次序運行。
1.2 什麼是線程同步
同步即協同步調,按預定的先後次序運行。
線程同步,指一個縣城發出某一功能調用時,在沒有得到結果之前,該調用不返回。同時其他線程爲保證數據一致性,不能調用該功能。
舉例1:銀行存款5000。櫃檯,折:取3000;提款機,卡:取3000.剩餘2000
舉例2:內存中100字節,線程T1欲填入全1,線程T2欲填入全0.但如果T1執行了50個字節失去cpu,T2執行,會將T1寫過的內容覆蓋。當T1再次獲得cpu繼續從失去cpu的位置向後寫入1,當執行結束,內存中的100字節,既不是全1,也不是全0。
產生的現象叫做“與時間有關的錯誤”(time related)。爲了避免這種數據混亂,線程需要同步。
“同步”的目的,是爲了避免數據混亂,解決與時間有關的錯誤,實際上,不僅線程間需要同步,進程間、信號間等等都需要同步機制。
因此,所有“多個控制流,共同操作一個共享資源”的情況,都需要同步。
1.3 多線程出現數據混亂(數據競爭)的原因
- 資源共享(獨享資源則不會)
- 調度隨機(意味着數據訪問會出現競爭者)
- 線程間缺乏必要的同步機制
以上三點中前兩點不能改變,想提高效率,傳遞數據,資源必須共享。只要共享資源,就一定會出現競爭。只要存在競爭關係,數據就很容易出現混亂。
所以只能從第三點着手解決。使多個線程在訪問共享資源的時候出現互斥。
2. 互斥量mutex
- Linux提供一把互斥鎖
mutex
(也稱之爲互斥量) - 每個線程在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束後解鎖。
- 資源還是共享的,線程間也還是競爭的,但通過鎖將資源的訪問變爲互斥操作,而後與時間有關的錯誤也不會在產生了。
但是應該注意:同一個時刻,只能有一個線程持有該鎖。
當A
線程對某個全局變量加鎖訪問,B
在訪問前嘗試加鎖,拿不到鎖,B
阻塞。C
線程不去加鎖,而直接訪問該全局變量,依然能夠訪問,但會出現數據混亂。
所以,互斥鎖實質上是是操作系統提供的一把“建議鎖”(又稱“協同所”),建議程序中有多線程訪問共享資源的時候使用該機制,但是,並沒有強制限定。
2.1 mutex相關的函數和使用步驟
pthread_mutex_t
類型,其本質是一個結構體,爲簡化理解,應用時可忽略其實現細節,簡單當成整數看待。
pthread_mutex_t mutex
:變量mutex
只有兩種取值0
、1
;
pthread_mutex_init 初始化
pthread_mutex_destroy 摧毀
pthread_mutex_lock 加鎖
pthread_mutex_unlock 解鎖
互斥量的使用步驟:
- 初始化
- 加鎖
- 執行邏輯——操作共享數據
- 解鎖
注意事項:
加鎖需要最小粒度,不要一直佔用臨界區(加鎖到解鎖之間的執行邏輯即爲臨界區)
2.1.1 初始化鎖
功能:初始化一個互斥鎖(互斥量);–>初值可看做1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
返回值: 若成功,返回0,否則,返回錯誤編號
或者
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
參數:
mutex 傳出參數,互斥量——鎖
restruct 只用於限制指針,告訴編譯器,所有修改該指針指向內存中內容的操作,只能通過本指針完成。不能通過除本指針以外的其他變量或指針修改。
attr 互斥屬性。是一個傳入參數,通常傳NULL,選用默認屬性(線程間共享).
2.1.2 給共享資源加鎖解鎖
這些pthread_mutex_t *mutex
參數都是init
初始化的那個鎖。
返回值: 若成功,返回0,否則,返回錯誤編號
pthread_mutex_lock(pthread_mutex_t *mutex)
功能:
如果當前未鎖,成功,加鎖,可理解爲將mutex--(或-1)
如果當前已鎖,阻塞等待,鎖被打開之後,線程解除阻塞。
pthread_mutex_unlock(pthread_mutex_t *mutex)
功能:
解鎖。可理解爲將mtex++(或+1),同時將阻塞在該鎖上的所有線程全部喚醒
pthread_mutex_trylock(pthread_mutex_t *mutex)
功能:
沒有鎖上:當前線程會給這把鎖加鎖
如果鎖上了:不會阻塞,返回
lock與unlock:
- lock嘗試加鎖,如果加鎖不成功,線程阻塞,阻塞到持有該互斥量的其他線程解鎖爲止。
- unlock主動解鎖,同時將阻塞到該鎖上所有線程全部喚醒,至於哪個線程先被喚醒取決於優先級,調度。默認:先阻塞、先喚醒。
例如:T1 T2 T3 T4 使用一把mutex鎖,T1加鎖成功,其他線程均阻塞,直至T1解鎖,T1解鎖後,T2 T3 T4均被喚醒,並自動再次嘗試加鎖。
可假想mutex鎖init成功初值爲1。lock功能是將mutex–。unlock將mutex++。
2.2.3 摧毀鎖
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值: 若成功,返回0,否則,返回錯誤編號
參數也是init
初始化的那個鎖。
2.2 互斥量使用的例子
通過互斥量,兩個線程交替打印
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
//常量初始化鎖——mutex(這樣就不用init函數了),將其定義爲全局變量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int sum=0;
void *thr1(void *arg){
while(1){
//先上鎖
pthread_mutex_lock(&mutex);//加鎖,當有線程已經加鎖的時候會阻塞
//加鎖到解鎖這一段爲臨界區
printf("hello");
sleep(rand()%3);
printf("world\n");
//釋放鎖
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
}
void *thr2(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("HELLO");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
}
int main(){
pthread_t tid[2];
pthread_create(&tid[0],NULL,thr1,NULL);
pthread_create(&tid[1],NULL,thr2,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
return 0;
}
運行結果:
如果不加鎖:
2.3 pthread_mutex_trylock
pthread_mutex_unlock(pthread_mutex_t *mutex)
lock與trylock:
- lock加鎖失敗會阻塞,等待鎖釋放。
- trylock加鎖失敗直接返回錯誤號(如EBUSY),不阻塞。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
pthread_mutex_t mutex;
void *thr(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("hello world\n");
sleep(30);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_t tid;
pthread_create(&tid,NULL,thr,NULL);
sleep(1);
while(1){
int ret = pthread_mutex_trylock(&mutex);
//加鎖失敗
if(ret > 0){
printf("ret = %d,srrmsg:%s\n",ret,strerror(ret));
}
sleep(1);
}
return 0;
}
運行結果:
返回值的錯誤碼是16
,我們來看一下16
代表什麼意思。
錯誤碼定義的地方:
/usr/include/asm-generic/errno-base.h
/usr/include/asm-generic/errno.h
/usr/include/errno.h
3. 死鎖
產生條件
-
鎖了又鎖,自己加了鎖,自己又加了一把鎖。一般都是有分支或者寫代碼的時候忘了會出現這個問題
-
交叉鎖(如下圖所示)——解決方案:1、每個線程申請鎖的順序要一致;2、如果申請到一把鎖,另一個申請不到,則釋放已有資源
4. 讀寫鎖
讀共享,寫互斥,寫的優先級高。適合讀的線程多的場景
讀寫鎖仍然是一把鎖,有不同的狀態:未加鎖、讀鎖、寫鎖。
4.1 讀寫鎖特性
- 讀寫鎖是“寫模式加鎖”時,解鎖前,所有對該鎖加鎖的線程都會被阻塞。
- 讀寫鎖是“讀模式加鎖”時,如果線程以讀模式對其加鎖會成功,如果線程以寫模式加鎖會阻塞。
場景例子:
(1)線程A加讀鎖成功,又來了三個線程,做讀操作,可以加鎖成功【讀共享 - 並行處理】
(2)線程A加寫鎖成功,又來了三個線程,做讀操作,三個線程阻塞 【寫獨佔 】
(3)線程A加讀鎖成功,又來了B線程加寫鎖阻塞,又來了C線程加讀鎖阻塞【讀寫不能同時;寫的優先級高(寫鎖都阻塞了那麼之後的讀鎖肯定阻塞)】
4.2 讀寫鎖使用場景
普遍讀寫鎖在讀的線程居多的時候使用。
讀:並行
寫:串行
讀寫鎖練習場景:
- 線程A加寫鎖成功,線程B請求讀鎖
線程B阻塞 - 線程A持有讀鎖,線程B請求寫鎖
線程B阻塞 - 線程A持有讀鎖,線程B請求讀鎖
線程B加鎖成功 - 線程A持有讀鎖,然後線程B請求寫鎖,然後線程C請求讀鎖
B阻塞,C阻塞 - 寫的優先級高
A解鎖,B線程加寫鎖成功
B解鎖,C加讀鎖成功 - 線程A持有寫鎖,然後線程B請求讀鎖,然後線程C請求寫鎖
BC阻塞
A解鎖,C加寫鎖成功,B繼續阻塞
C解鎖,B加讀鎖
4.3 讀寫鎖主要操作函數
初始化讀寫鎖:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
或者:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
銷燬讀寫鎖:
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加讀鎖:
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
阻塞:之前對這把鎖加的寫鎖的操作
嘗試加讀鎖:
pthread_rwlock_trydlock(pthread_rwlock_t *rwlock);
返回值:
加鎖成功 0
失敗 錯誤號
加寫鎖:
pthread_rwlock_wrlock(pthread_rwlock_t *rwlocl);
上一次加鎖寫鎖,還沒有解鎖的時候
上一次加讀鎖,沒解鎖
嘗試加寫鎖:
pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
解鎖:
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
4.4 讀寫鎖例子
5個讀,3個寫
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int beginnum = 1000;
void *thr_write(void *arg) {
while(1){
//寫鎖加鎖
pthread_rwlock_wrlock(&rwlock);
printf("---%s---self---%lu---beginnum---%d\n",__FUNCTION__,pthread_self(),++beginnum);
usleep(2000);//模擬佔用時間
//解鎖
pthread_rwlock_unlock(&rwlock);
usleep(4000);
}
return NULL;
}
void *thr_read(void *arg) {
while(1){
//讀鎖加鎖
pthread_rwlock_rdlock(&rwlock);
printf("---%s---self---%lu---beginnum---%d\n",__FUNCTION__,pthread_self(),beginnum);
usleep(2000);//模擬佔用時間
//解鎖
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(){
//創建5個讀鎖和3個寫鎖
int n =8,i = 0;
pthread_t tid[8];//5-read ,3-write
for(i = 0; i < 5; i ++){
//參數依次是線程地址、線程屬性、函數名、傳入的參數
pthread_create(&tid[i],NULL,thr_read,NULL);
}
for(;i < 8; i ++){
//參數依次是線程地址、線程屬性、函數名、傳入的參數
pthread_create(&tid[i],NULL,thr_write,NULL);
}
for(i = 0; i < 8;i ++){
//線程回收
pthread_join(tid[i],NULL);
}
return 0;
}
執行結果:
5. 條件變量與消費者模式
條件變量概念是線程掛起直到共享數據的某些條件得到滿足。
引入條件變量原因:mutex
會產生如下問題:
多個線程搶到鎖之後,發現並沒有資源,因此釋放鎖,然後繼續多線程搶鎖,然後釋放鎖,這戶造成資源的浪費。
條件變量一般與互斥量協同作用
- 使用條件變量+互斥量
互斥量: 保護一塊共享資源
條件變量:引起阻塞
生產者和消費者模型
條件變量的兩個動作?
- 條件不滿足,阻塞線程
- 當條件滿足,通知阻塞的線程開始工作
條件變量的類型:pthread_cond_t
;
5.1 主要函數
pthread_cond_init 初始化
pthread_cond_destroy 銷燬一個條件變量
pthread_cond_wait 阻塞等待一個條件變量
pthread_cond_timewait 限時等待一個條件變量
pthread_cond_signal 喚醒至少一個阻塞在條件變量上的線程
pthread_cond_broadcast 喚醒全部阻塞在條件變量上的線程
5.1.1 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
或者:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
5.1.2 銷燬一個條件變量
pthread_cond_destroy(pthread_cond_t *cond);
5.1.3 阻塞等待一個條件變量
pthread_cond_wait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex
);
(1)阻塞線程
(2)將已經上鎖的mutex解鎖
(3)該函數解除阻塞,會對互斥鎖加鎖
5.1.4 限時等待一個條件變量
pthread_cond_timewait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime
);
參看 man sem_timedwait
函數,查看struct timespec
結構體
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
tv_sec絕對時間,填寫的時候time(NULL)+600 ==>設置超時600s
5.1.5 喚醒至少一個阻塞在條件變量上的線程
pthread_cond_signal(pthread_cond_t *restrict cond);
5.1.6 喚醒全部阻塞在條件變量上的線程
pthread_cond_broadcast(pthread_cond_t *cond);
5.2 條件變量解決生產着消費者模式
用鏈表存儲數據
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
int beginnum = 1000;
//初始化mutex
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
//初始化條件變量
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
typedef struct _ProdInfo{
int num;
struct _ProdInfo *next;
}ProdInfo;
ProdInfo *HEAD = NULL;
void *thr_producer(void *arg){
//負責向鏈表添加數據
while(1){
ProdInfo *prod=(ProdInfo *)malloc(sizeof(ProdInfo));
prod->num=beginnum++;
//上鎖
pthread_mutex_lock(&mutex);
//add to list
prod->next=HEAD;
HEAD=prod;
printf("--------%s--------seld=%lu------------%d\n",__FUNCTION__,pthread_self(), prod->num);
pthread_mutex_unlock(&mutex);
//發起通知
pthread_cond_signal(&cond);
sleep(rand()%4);
}
return NULL;
}
void *thr_customer(void *arg){
ProdInfo *prod=NULL;
while(1){
//取鏈表的數據
pthread_mutex_lock(&mutex);
//判斷有沒有數據
if(HEAD==NULL){
//發送消息等待
pthread_cond_wait(&cond, &mutex);
}
//此時鏈表非空
prod=HEAD;
HEAD=HEAD->next;
printf("--------%s--------seld=%lu------------%d\n",__FUNCTION__,pthread_self(), prod->num);
//解鎖
pthread_mutex_unlock(&mutex);
sleep(rand()%4);
free(prod);
}
return NULL;
}
int main(){
pthread_t tid[2];
pthread_create(&tid[0], NULL, thr_producer, NULL);
pthread_create(&tid[1], NULL, thr_customer, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
運行結果:
一個生產者多個消費者時
把thr_customer
中的
if(HEAD==NULL)
改成
while(HEAD==NULL)
即可。
6. 信號量
加強版的互斥鎖。適於多個資源多個線程訪問的情況。
6.1 主要函數
sem_init
sem_destroy
sem_wait
sem_trywait
sem_timedwait
sem_post
以上六個函數的返回值都是:
成功返回0
,失敗返回-1
,同時設置errno
。(注意,他們沒有pthread前綴)
sem_t
類型,本質仍是結構體。但應用期間可簡單看作爲整數,忽略實現細節(類似於使用文件描述符)。
sem_t sem;
規定信號量sem
不能小於0,。頭文件<semaphore.h>
6.1.1 初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem 定義的信號量,傳出
pshared 非0則代表進程信號量, 0代表線程信號量
value 定義信號量的併發數(鑰匙的個數)
6.1.2 摧毀
int sem_destroy(sem_t *sem);
摧毀一個信號量
6.1.3 申請信號量,申請成功,則value–
int sem_wait(sem_t *sem);
當信號量爲0的時候,阻塞
6.1.4 釋放信號量 value++
int sem_post(sem_t *sem);
6.2 信號量基本操作
sem_wait
:(類比pthread_mutex_lock
)
- 信號量大於
0
,對信號量--
- 信號量等於
0
,造成線程阻塞
sem_post
:將信號量++,
同時喚醒阻塞在信號量上的線程(類比pthread_mutex_unlock
)
但,由於sem_t
的實現對用戶隱藏,所以所謂的++
,--
操作只能通過函數來實現,而不能直接++
,--
符號。
以上操作也成爲PV操作
信號量的初值,決定了佔用信號量的線程的個數。
6.3 信號量實現生產者消費者模式
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
//blank只有多少可以放生產者生產東西的地方,xfull是消費者可以消費的東西的數量
sem_t blank,xfull;
#define _SEM_CNT_ 5
// 模擬餅筐
int queue[_SEM_CNT_];
int beginnum = 100;
void *thr_producter(void *arg) {
int i = 0;
while(1){
//申請資源 blank--
//看看能不能生產,還有沒有空間
sem_wait(&blank);
//打印函數名、線程名、數量
printf("-----%s-----self--%lu----num----%d\n",__FUNCTION__,pthread_self(),beginnum);
//生產數據
queue[(i++)%_SEM_CNT_] = beginnum++;
//xfull ++
sem_post(&xfull);
sleep(rand()%3);
}
return NULL;
}
void *thr_customer(void *arg) {
int i = 0;
int num = 0;
while(1){
//看看能不能消費
sem_wait(&xfull);
//通過取餘來
num = queue[(i++)%_SEM_CNT_];
printf("-----%s-----self--%lu----num----%d\n",__FUNCTION__,pthread_self(),num);
//發送信號
sem_post(&blank);
sleep(rand()%3);
}
return NULL;
}
int main(){
//線程,所有第二個參數是0, 如果是進程,則第二個參數是非0
sem_init(&blank,0,_SEM_CNT_);
//消費者一開始的初始化默認沒有產品
sem_init(&xfull,0,0);
pthread_t tid[2];
//線程沒有設置屬性,所有第二個參數爲NULL
pthread_create(&tid[0],NULL,thr_producter,NULL);
pthread_create(&tid[1],NULL,thr_customer,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
sem_destroy(&blank);
sem_destroy(&xfull);
return 0;
}
運行結果:
6.4 信號量與互斥量的區別
No. | 區別 | 信號量 | 互斥量 |
---|---|---|---|
1 | 使用對象 | 線程和進程 | 線程 |
2 | 最值 | 非負整數 | 0或1 |
3 | 操作 | PV操作可由不同線程完成 | 加鎖和解鎖必須由同一線程使用 |
4 | 應用 | 用於線程的同步 | 用於線程的互斥 |
- 互斥:主要關注於資源訪問的唯一性和排他性。
- 同步:主要關注於操作的順序,同步以互斥爲前提。
7. 文件鎖
藉助fcntl
函數來實現鎖機制。操作文件的進程沒有獲得鎖時,可以打開,但無法執行read、write操作。
適合環境——當前系統中該進程只能起一個。
實現原理——當一個進程打開了這個文件,另一個進程發現文件被打開了,就無法再打開這個文件了。
文件鎖——讀共享,寫獨佔。
7.1 fcntl函數
int fcntl(int fd, int cmd, ... /* arg */ );
參1 文件描述符
參2
F_SETLK(struct flock *) 設置文件鎖(trylock)
F_SETLKW(struct flock *) 設置文件鎖(lock) W-->wait
F_GETTLK(struct flock *) 獲取文件鎖
參3
struct flock {
...
short l_type; 鎖的類型:F_RDLCK、F_WRLCK、F_UNLCK
short l_whence; 偏移位置:SEEK_SET、SEEK_CUR、SEEK_END
off_t l_start; 起始偏移:0
off_t l_len; 長度:0表示整個文件加鎖
pid_t l_pid; 持有該所的進程ID:(F_GETLK only)
...
};
以上可以man fcntl
查看
7.2 文件鎖舉例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#define _FILE_NAME_ "/home/itheima/temp.lock"
int main() {
int fd = open(_FILE_NAME_,O_RDWR|O_CREAT,0666);
if(fd < 0){
//文件打開失敗
perror("open err");
return -1;
}
struct flock lk;
lk.l_type = F_WRLCK;
lk.l_whence =SEEK_SET ;
lk.l_start = 0;
lk.l_len =0;
if(fcntl(fd,F_SETLK,&lk) < 0){
perror("get lock err");
exit(1);
}
// 核心邏輯
while(1){
printf("I am alive!\n");
sleep(1);
}
return 0;
}
開啓另外一個終端