費縣做網(wǎng)站收錄提交入口網(wǎng)址
12. 多線程編程
注:博客中有書中沒有的內(nèi)容,均是來自
黑馬06-線程概念_嗶哩嗶哩_bilibili
早期Linux不支持線程,直到1996年,Xavier Leroy等人開發(fā)出第一個基本符合POSIX標準的線程庫LinuxThreads,但LinuxThreads效率低且問題多,自內(nèi)核2.6開始,Linux才開始提供內(nèi)核級的線程支持,并有兩個組織致力于編寫新的線程庫:NGPT(Next Generation POSIX Threads)和NPTL(Native POSIX Thread Library),但前者在2003年就放棄了,因此新的線程庫就是NPTL。NPTL比LinuxThreads效率高,且更符合POSIX規(guī)范,所以它已經(jīng)成為glibc的一部分,本書使用的線程庫是NPTL。
本章要討論的線程相關的內(nèi)容都屬于POSIX線程(簡稱 pthread)標準,不局限于NPTL實現(xiàn),包括:
- 創(chuàng)建線程和結(jié)束線程;
- 讀取和設置線程屬性;
- POSIX線程同步方式:POSIX信號量、互斥鎖和條件變量。
文章目錄
- 12. 多線程編程
- 1.線程概念
- 1.什么是線程
- 2.Linux內(nèi)核線程實現(xiàn)原理
- 3.線程共享資源
- 4.線程優(yōu)缺點
- 2.Linux 線程概述
- 1.線程模型
- 2.Linux 線程庫
- 3.線程控制原語
- 1.pthread_self
- 2.pthread_create
- 循環(huán)創(chuàng)建多個子線程
- 3.pthread_exit
- 4.pthreda_join
- 5pthread_detach
- 6.pthread_cancel
- 7.終止線程方式
- 8.控制原語對比
- 4.線程屬性
- 1.概述
- 2.線程屬性初始化和銷毀
- 3.線程的分離狀態(tài)
- 線程分離狀態(tài)的函數(shù)
- 4.線程屬性控制示例
- 5.線程使用注意事項
- 以下是線程同步的內(nèi)容:
- 6.同步概念
- 同步
- 線程同步
- 7.互斥鎖
- 1.概述
- 2.互斥鎖基礎 API
- 1.初始化和銷毀
- 2.加鎖解鎖
- 3.使用案例
- 4.注意事項
- 5.try鎖
- 3.互斥鎖屬性
- 4.死鎖
- 8.讀寫鎖
- 1.原理
- 2.特性
- 3.對應函數(shù)
- 1.初始化和銷毀
- 2.加鎖解鎖
- 4.示例
- 9.條件變量
- 1.工作原理
- 2.對應函數(shù)
- 1.總覽
- 2.創(chuàng)建和銷毀
- 3.wait函數(shù)
- 4.pthread_cond_timedwait 函數(shù)
- 3.使用條件變量模擬實現(xiàn)生產(chǎn)者—消費者問題
- 4.條件變量優(yōu)勢
- 9.POSIX 信號量
- 1.概述
- 2.對應函數(shù)
- 1.總覽
- 2.初始化和銷毀
- 3.PV操作主要函數(shù)
- 3.實現(xiàn)生產(chǎn)者消費者
- 10.線程同步機制包裝類
- 11.多線程環(huán)境
- 1.可重入函數(shù)
- 2.進程和線程
- 人話翻譯一下它這三個函數(shù)在干什么
- 總結(jié)
- 3.線程和信號
1.線程概念
1.什么是線程
LWP:
light weight process 輕量級的進程,本質(zhì)仍是進程(在Linux環(huán)境下)
進程:獨立地址空間,擁有PCB
線程:有獨立的PCB,但沒有獨立的地址空間(與進程共享)
區(qū)別:在于是否共享地址空間。獨居(地址空間就是進程一個) ; 合租(地址空間有多個線程)
Linux 下:
線程:最小的執(zhí)行(調(diào)度)單位
進程:最小分配資源單位,可看成是只有一個線程的進程
執(zhí)行該命令可以得到線程號LWP(LWP是cpu執(zhí)行的最小單位,但和后面講到的線程id不是一回事)
ps aux
ps -Lf 進程id //得到該進程里面的線程
圖中LWP就是線程號,可以看到是接著4388和4213往后寫的,所以其實內(nèi)核把這些進程的線程當做進程看,所以更容易獲得CPU
2.Linux內(nèi)核線程實現(xiàn)原理
類Unix系統(tǒng)中,早期是沒有“線程”概念的,80年代才引入,借助進程機制實現(xiàn)出了線程的概念。因此在這類系統(tǒng)中,進程和線程關系密切
1.輕量級進程(light-weight process),也有PCB,創(chuàng)建線程使用的底層函數(shù)和進程一樣,都是clone
2.從內(nèi)核里看進程和線程是一樣的,都有各自不同的PCB,但是PCB中指向內(nèi)存資源的三級頁表是相同的
? 這句話中關于進程和線程都有各自不同的 PCB 部分正確,但線程的 PCB 相對進程的 PCB 有很大差異且不那么獨立;而說進程和線程指向內(nèi)存資源的三級頁表相同是錯誤的,應該是一個進程內(nèi)的線程共享進程的頁表,不同進程之間頁表是不同的。
3.進程可以蛻變成線程
4.線程可看做寄存器和的集合
5.在linux下,線程最是小的執(zhí)行單位;進程是最小的分配資源單位
三級映射:進程PCB->頁目錄->頁表->物理頁面
3.線程共享資源
線程共享資源
-
1.文件描述符表
-
2.每種信號的處理方式
-
3.當前工作目錄
-
4.用戶ID和組 ID
-
5.內(nèi)存地址空間( 具體共享的部分是: .text/.data/.bss/heap/共享庫)------>線程間共享全局變量
線程非共享資源
-
1.線程id
-
2.處理器現(xiàn)場和棧指針(內(nèi)核棧)
-
3.獨立的空間(用戶空間拔)
-
4.errno 變量
-
5.信號屏蔽字
-
6.調(diào)度優(yōu)先級
不推薦信號和線程一起用,最好就別混著用。
4.線程優(yōu)缺點
優(yōu)點:
-
提高程序并發(fā)性
-
開銷小
-
數(shù)據(jù)通信、共享數(shù)據(jù)方便
缺點:
-
庫函數(shù),不如系統(tǒng)調(diào)用穩(wěn)定性強
-
調(diào)試、編寫困難、gdb不支持
-
對信號支持不好
優(yōu)點相對突出,缺點均不是硬傷。Linux下由于實現(xiàn)方法導致進程、線程差別不是很大
如果線程進程都可用,那優(yōu)先選擇線程
2.Linux 線程概述
1.線程模型
線程是程序中完成一個獨立任務的完整執(zhí)行序列,即一個可調(diào)度的實體。根據(jù)運行環(huán)境和調(diào)度者的身份,線程可分為內(nèi)核線程和用戶線程。
- 內(nèi)核線程在有的系統(tǒng)上也稱為LWP(Light Weight Process,輕量級進程),運行在內(nèi)核空間,由內(nèi)核調(diào)度;
- 用戶線程運行在用戶空間,由線程庫調(diào)度。當進程的一個內(nèi)核線程獲得CPU的使用權時,它就加載并運行一個用戶線程,可見,內(nèi)核線程相當于用戶線程運行的容器,一個進程可以擁有M個內(nèi)核線程和N個用戶線程,其中M<=N,并且在一個系統(tǒng)的所有進程中,M和N的比值都是固定的。
按照M:N的取值,線程的實現(xiàn)可分為三種模式:完全在用戶空間實現(xiàn)、完全由內(nèi)核調(diào)度、雙層調(diào)度(two level scheduler)。
完全在用戶空間實現(xiàn)的線程無須內(nèi)核的支持,內(nèi)核甚至不知道這些線程的存在,線程庫負責管理所有執(zhí)行線程,如線程的優(yōu)先級、時間片等。線程庫利用longjmp
函數(shù)來切換線程的執(zhí)行,使它們看起來像是并發(fā)執(zhí)行的,但實際上內(nèi)核仍然是把整個進程作為最小單位來調(diào)度,換句話說,一個進程中的所有線程共享該進程的時間片,它們對外表現(xiàn)出相同的優(yōu)先級(即所有線程使用相同的優(yōu)先級,因為它們都是以進程為單位調(diào)度的)。對這種實現(xiàn)方式而言,M=1,即 N 個用戶線程對應一個內(nèi)核線程,而該內(nèi)核線程對實際就是進程本身。完全在用戶空間實現(xiàn)的線程的優(yōu)點:創(chuàng)建和調(diào)度線程都無需內(nèi)核干預,因此速度快,且它不占用額外內(nèi)核資源,所以即使一個進程創(chuàng)建了很多線程,也不會對系統(tǒng)性能造成明顯的影響。其缺點是:對于多處理器系統(tǒng),一個進程的多個線程無法運行在不同的CPU上,因為內(nèi)核是按照其最小調(diào)度單位來分配CPU的,且線程的優(yōu)先級只在相對于進程內(nèi)的其他線程生效,比較不同進程內(nèi)的線程的優(yōu)先級無意義。
完全由內(nèi)核調(diào)度的模式將創(chuàng)建、調(diào)度線程的任務交給了內(nèi)核,運行在用戶空間的線程庫無須執(zhí)行管理任務,這與完全在用戶空間實現(xiàn)的線程恰恰相反,因此二者的優(yōu)缺點也正好互換。較早的Linux內(nèi)核對內(nèi)核線程的控制能力有限,線程庫通常還要提供額外的控制能力,尤其是線程同步機制,但現(xiàn)代Linux內(nèi)核已經(jīng)大大增強了對線程的支持。完全由內(nèi)核調(diào)度的線程實現(xiàn)滿足M : N=1 : 1,即1個用戶空間線程被映射為1個內(nèi)核線程。
雙層調(diào)度模式是兩種實現(xiàn)模式的混合體,內(nèi)核調(diào)度M個內(nèi)核線程,線程庫調(diào)度N個用戶線程,這種線程實現(xiàn)方式結(jié)合了前兩種方式的優(yōu)點,不會消耗過多的內(nèi)核資源,且線程切換速度也較快,同時還能充分利用多處理器優(yōu)勢。
完全在用戶空間實現(xiàn):一個內(nèi)核級線程對多個用戶級別線程
完全由內(nèi)核調(diào)度:一個內(nèi)核級線程對一個用戶級線程
雙層調(diào)度:多個內(nèi)核級線程對多個用戶級線程
2.Linux 線程庫
Linux上兩個有名的線程庫是LinuxThreads和NPTL,它們都采用1:1方式實現(xiàn)(完全由內(nèi)核調(diào)度的模式)?,F(xiàn)代Linux上默認使用的線程庫是NPTL,用戶可用以下命令查看當前系統(tǒng)使用的線程庫:
getconf GNU_LIBPTHREAD_VERSION
LinuxThreads線程庫的內(nèi)核線程是用clone
系統(tǒng)調(diào)用創(chuàng)建的進程模擬的,clone
系統(tǒng)調(diào)用和fork
系統(tǒng)調(diào)用的作用類似,都創(chuàng)建調(diào)用進程的子進程,但我們可以為clone
系統(tǒng)調(diào)用指定CLONE_THREA
D標志,此時它創(chuàng)建的子進程與調(diào)用進程共享相同的虛擬地址空間、文件描述符、信號處理函數(shù),這些都是線程的特點,但用進程模擬內(nèi)核線程會導致很多語義問題:
- 每個線程擁有不同的PID,不符合POSIX規(guī)范。
- Linux信號處理本來是基于進程的,但現(xiàn)在一個進程內(nèi)部的所有線程都能且必須處理信號。
- 用戶ID、組ID對一個進程中的不同線程來說可能不同。
- 進程產(chǎn)生的核心轉(zhuǎn)儲文件不會包含所有線程的信息,而只包含該核心轉(zhuǎn)儲文件的線程的信息。
- 由于每個線程都是一個進程,因此系統(tǒng)允許的最大進程數(shù)就是最大線程數(shù)。
LinuxThreads線程庫一個有名的特性是所謂的管理線程,它是進程中專門用于管理其他工作線程的線程,其作用為:
- 系統(tǒng)發(fā)送給進程的終止信號先由管理線程接收,管理線程再給其他工作線程發(fā)送同樣的信號以終止它們。
- 當終止工作線程或工作線程主動退出時,管理線程必須等待它們結(jié)束,以避免僵尸進程。
- 如果主線程即將先于其他工作線程退出,則管理線程將阻塞主線程,直到所有其他工作線程都結(jié)束后才喚醒它。
- 回收每個線程堆棧使用的內(nèi)存。
管理線程的引入,增加了額外的系統(tǒng)開銷,且由于管理線程只能運行在一個CPU上,所以LinuxThreads線程庫不能充分利用多處理器系統(tǒng)的優(yōu)勢(所有管理操作只能在一個CPU上完成)。
要解決LinuxThreads線程庫的一系列問題,不僅需要改進線程庫,最主要的是需要內(nèi)核提供更完善的線程支持,因此Linux內(nèi)核從2.6版本開始,提供了真正的內(nèi)核線程,新的NPTL線程庫也應運而生,相比LinuxThreads,NPTL的主要優(yōu)勢在于:
- 內(nèi)核線程不再是一個進程,因此避免了很多用進程模擬線程導致的語義問題。
- 摒棄了管理線程,終止線程、回收線程堆棧等工作都可以由內(nèi)核完成。
- 由于不存在管理線程,所以一個進程的線程可以運行在不同CPU上,從而充分利用了多處理器系統(tǒng)的優(yōu)勢。
- 線程的同步由內(nèi)核來完成,隸屬于不同進程的線程之間也能共享互斥鎖,因此可實現(xiàn)跨進程的線程同步。
3.線程控制原語
創(chuàng)建和結(jié)束線程的API在Linux上定義在pthread.h
頭文件。
1.pthread_self
獲取當前線程的線程ID。其作用對應進程中getpid()函數(shù)。
#include<pthread.h>
pthread_t pthread_self(void);
返回值:
成功:0;失敗:無 !
線程ID:pthread_t類型,本質(zhì):在Linux下為無符號整數(shù)(%lu),其他系統(tǒng)中可能是結(jié)構(gòu)體實現(xiàn)。
線程ID是進程內(nèi)部識別線程的標志。(兩個進程間,線程ID允許相同)
注意:不應使用全局變量 pthread_t tid,在子線程中通過 pthread_create 傳出參數(shù)來獲取線程 ID,而應使用pthread self
2.pthread_create
pthread_create
函數(shù)創(chuàng)建一個線程:
#include <pthread.h>
int pthread_create(pthread_t* thread, count pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
參數(shù)
thread
:傳出參數(shù),表示新創(chuàng)建的子線程的線程id,新線程的標識符,后續(xù)pthread_*
函數(shù)通過它來引用新線程,其類型pthread_t
定義如下:
#include <bits/pthreadtypes.h>
typedef unsignde long int ptherad_t;
attr
:用于設置新線程的屬性,給它傳遞NULL
表示使用默認線程屬性。
start_routine
:子線程的回調(diào)函數(shù)
arg
:子線程回調(diào)函數(shù)的參數(shù),沒有的話傳一個NULL
返回值
成功時返回0,失敗時返回錯誤碼,并且第一個參數(shù)thread不會有值。
一個用戶可以打開的線程數(shù)不能超過RLIMIT_NPROC
軟資源限制,此外,系統(tǒng)上所有用戶能創(chuàng)建的線程總數(shù)也不能超過/proc/sys/kernel/threads-max
內(nèi)核參數(shù)定義的值。
循環(huán)創(chuàng)建多個子線程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include<pthread.h>void *tfn(void *arg)
{int i=(int)arg;sleep(i);printf("I'm %dth thread:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());return NULL;
}int main(int argc,char* argv[])
{int i,ret;pthread_t tid;for(i=0;i<5;i++){ret=pthread_create(&tid,NULL,tfn,(void*)i);//ret=pthread_create(&tid,NULL,tfn,(void*)&i);//這樣寫是不行的,因為傳入的是i的地址,但是i是循環(huán)變量,一直在變化,可能現(xiàn)在傳入的是2,但是等子線程讀取的時候就是3或4了if(ret!=0){perror("pthread_create error");exit(1);}}sleep(i);printf("main: pid=%d,tid=%d\n",getpid(),pthread_self());return 0;
}
arg傳值采用值傳遞,借助強轉(zhuǎn)
3.pthread_exit
線程一旦被創(chuàng)建,內(nèi)核就可以調(diào)度內(nèi)核線程來執(zhí)行start_routine
函數(shù)指針參數(shù)所指向的函數(shù)了,線程函數(shù)在結(jié)束時最好調(diào)用pthread_exit
函數(shù),以確保安全、干凈地退出:
#include <pthread.h>
void pthread_exit(void* retval);
pthread_exit
函數(shù)會通過retval
參數(shù)向線程的回收者傳遞其退出信息,它執(zhí)行完后不會返回到調(diào)用者,且永遠不會失敗。
參數(shù):
retval:回調(diào)函數(shù)的返回值。 無返回值時,傳NULL
辨析:
exit(); 退出當前進程
return: 返回到調(diào)用者那里去
pthread_exit(): 退出當前線程
4.pthreda_join
一個進程中的所有線程都能調(diào)用pthread_join
函數(shù)來回收其他線程(前提是目標線程是可回收的),即等待其他線程結(jié)束,這類似回收進程的wait
和waitpid
系統(tǒng)調(diào)用。
阻塞等待線程退出,獲取線程狀態(tài)。
#include <pthread.h>
int pthread_join(pthread_t thread, void** retval);
參數(shù)
thread
:目標線程的線程號(標識符)。
retval
:傳出參數(shù),目標線程返回的退出信息。就是接收pthread_exit的參數(shù)。
返回值
pthread_join
函數(shù)會一直阻塞,直到被回收的線程結(jié)束為止。
成功時返回0,失敗則返回錯誤碼。
使用示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include<pthread.h>struct thrd{int var;char str[256];
};void *tfn(void *arg)
{ struct thrd *tval;tval=malloc(sizeof(tval));tval->var=100;strcpy(tval->str,"hello pthread\n");return (void*)tval;
}int main(int argc,char* argv[])
{int i,ret;pthread_t tid;struct thrd *retval;ret=pthread_create(&tid,NULL,tfn,(void*)i);if(ret!=0){perror("pthread_create error");exit(1);}ret=pthread_join(tid,(void **)&retval);if(ret!=0){perror("pthread_join error");exit(1);}printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);return 0;
}
5pthread_detach
設置線程分離。當一個線程處于分離狀態(tài)時,它的資源在結(jié)束時會自動被系統(tǒng)回收,而不需要其他線程調(diào)用pthread_join
函數(shù)來等待它結(jié)束并回收資源。
int pthread_detach(pthread_t thread);
參數(shù):
thread: 待分離的線程id
返回值:
成功:0
失敗:errno
非分離狀態(tài)(默認情況):在默認情況下,線程是處于非分離狀態(tài)(也稱為可連接狀態(tài))。當一個線程創(chuàng)建后,如果不進行pthread_detach
操作,那么它的資源(如線程棧等)在結(jié)束后不會自動釋放,需要其他線程通過pthread_join
函數(shù)來等待它結(jié)束,并且在pthread_join
函數(shù)返回后,系統(tǒng)才會回收該線程的資源。這就好像線程結(jié)束后,它的 “后事”(資源回收)需要其他線程來幫忙處理。
分離狀態(tài):使用pthread_detach
函數(shù)將線程設置為分離狀態(tài)后,線程結(jié)束時就會自動釋放自身的資源。這類似于線程自己處理自己的 “后事”,不需要其他線程專門等待它并回收資源。例如,在一些簡單的多線程應用場景中,對于那些執(zhí)行完任務后就不需要再關注其結(jié)果的線程,將它們設置為分離狀態(tài)可以簡化程序的邏輯,避免因為忘記調(diào)用pthread_join
而導致資源泄漏。
1.進程若有該機制,將不會產(chǎn)生僵尸進程。僵尸進程的產(chǎn)生主要由于進程死后,大部分資源被釋放,一點殘留資
源仍存于系統(tǒng)中,導致內(nèi)核認為該進程仍存在
2.**不能對一個已經(jīng)處于detach狀態(tài)的線程調(diào)用pthread_join,這樣的調(diào)用將返回EINVAL錯誤。**也就是說,如果已
經(jīng)對一個線程調(diào)用了pthread_detach就不能再調(diào)用pthread_join了。
3.也可使用 pthread_create函數(shù)參2(線程屬性)來設置線程分離
6.pthread_cancel
有時候我們希望異常終止一個線程,即取消(殺死)線程,它是通過pthread_cancel
函數(shù)實現(xiàn)的:
#include <ptrhead.h>
int pthread_cancel(pthread_t thread);
參數(shù)
thread
:要殺死的線程的線程號(標識符)
返回值
pthread_cancel
函數(shù)成功時返回0,失敗則返回錯誤碼。
接收到取消請求的目標線程可以決定是否允許被取消以及如何取消,這通過以下函數(shù)完成:
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
注意:
線程的取消并不是實時的,而有一定的延時。需要等待線程到達某個取消點(檢查點)
類似于玩游戲存檔,必須到達指定的場所(存檔點,如:客棧、倉庫、城里等)才能存儲進度。殺死線程也不是
立刻就能完成,必須要到達取消點。
取消點:是線程檢查是否被取消,并按請求進行動作的一個位置。通常是一些系統(tǒng)調(diào)用 creat,open,pause,
close,read,write … 執(zhí)行命令man 7 pthreads 可以查看具備這些取消點的系統(tǒng)調(diào)用列表。。心
**可粗略認為一個系統(tǒng)調(diào)用(進入內(nèi)核)即為一個取消點。**如線程中沒有取消點,可以通過調(diào)用 pthread_testcancel
函數(shù)自行設置一個取消點
被取消的線程,退出值定義在Linux的pthread庫中。常數(shù)PTHREAD_CANCELED的值是-1。可在頭文件pthread.h
中找到它的定義:#define PTHREAD_CANCELED((void*)-1)。因此當我們對一個已經(jīng)被取消的線程使用pthread_join回收時,得到的返回值為-1
示例:
如代碼所示,如果tfn中沒有設置保存點,也沒有進入內(nèi)核的語句(只是一些for if之類的),那cancel就會失效的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>void *tfn(void *arg)
{while(1){/*printf("I'm thread,pid:%d,tid:%lu\n",getpid(),pthread_self());sleep(1);*/pthread_testcancel();//設置保存點}return NULL;
}int main(void)
{pthread_t tid;//創(chuàng)建線程int ret = pthread_create(&tid,NULL,tfn,NULL);if(ret!=0){perror("pthread_create error");exit(1);}printf("I'm main,pid:%d,tid:%lu\n",getpid(),pthread_self());// 終止線程ret = pthread_cancel(tid);while(1);pthread_exit(NULL);
}
7.終止線程方式
總結(jié):終止某個線程而不終止整個進程,有三種方法:
-
從線程主函數(shù) return。這種方法對主控線程不適用,從 main 函數(shù) return 相當于調(diào)用 exit。
-
一個線程可以調(diào)用 pthread_cancel 終止同一進程中的另一個線程。
-
線程可以調(diào)用 pthread_exit 終止自己。
8.控制原語對比
4.線程屬性
1.概述
pthread_attr_t
結(jié)構(gòu)體定義了一套完整的線程屬性:
#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union{char __size[__SIZEOF_PTHREAD_ATTR_T];long int __align;
} pthread_attr_t;
線程的各種屬性都包括在一個字符數(shù)組里,線程庫定義了一系列函數(shù)來操作pthread_attr_t
類型變量,方便我們設置和獲取線程屬性:
#include <pthread.h>int pthread_attr_init ( pthread_attr_t* attr ); /* 初始化線程屬性對象 */
int pthread_attr_destroy(pthread_attr_t* attr ); /* 銷毀線程屬性對象。被銷毀的線程屬性對象只有再次初始化之后才能繼續(xù)使用 *//* 下面這些函數(shù)用于獲取和設置線程屬性對象的某個屬性 */
int pthread_attr_getdetachstate ( const pthread_attr_t* attr, int* detachstate );
int pthread_attr_setdetachstate( pthread_attr_t* attr, int detachstate );
int pthread_attr_getstackaddr( const pthread_attr_t* attr,void ** stackaddr );
int pthread_attr_setstackaddr( pthread_attr_t* attr, void* stackaddr );
int pthread_attr_getstacksize(const pthread_attr_t* attr, size_t* stacksize );
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);
int pthread_attr_getstack ( const pthread_attr_t* attr, void** stackaddr, size_t* stacksize);/* 還有很多,不在這里一一列舉了,具體可查看書籍P273頁 */
linux 下線程的屬性是可以根據(jù)實際項目需要,進行設置,之前我們討論的線程都是采用線程的默認屬性,默認屬性已經(jīng)可以解決絕大多數(shù)開發(fā)時遇到的問題。如我們對程序的性能提出更高的要求那么需要設置線程屬性,比如可以通過設置線程棧的大小來降低內(nèi)存的使用,增加最大線程個數(shù)。
typedef struct
{
int etachstate; //線程的分離狀態(tài)
int schedpolicy; //線程調(diào)度策略
struct sched_param
schedparam; //線程的調(diào)度參數(shù)
int inheritsched; //線程的繼承性
int scope; //線程的作用域
size_t guardsize; //線程棧末尾的警戒緩沖區(qū)大小int stackaddr_set; //線程的棧設置void* stackaddr; //線程棧的位置size_t stacksize; //線程棧的大小} pthread_attr_t;
主要結(jié)構(gòu)體成員:
-
線程分離狀態(tài)
-
線程棧大小(默認平均分配)
-
線程棧警戒緩沖區(qū)大小(位于棧末尾)
屬性值不能直接設置,須使用相關函數(shù)進行操作,初始化的函數(shù)為 pthread_attr_init,這個函數(shù)必須在
pthread_create 函數(shù)之前調(diào)用。之后須用 pthread_attr_destroy 函數(shù)來釋放資源。
**線程屬性主要包括如下屬性:**作用域(scope)、棧尺寸(stack size)、棧地址(stack address)、優(yōu)先級(priority)、分離的狀態(tài)(detached state)、調(diào)度策略和參數(shù)(scheduling policy and parameters)。默認的屬性為非綁定、非分離、缺省的堆棧、與父進程同樣級別的優(yōu)先級。
最重要的部分,不過太麻煩了,不如detach
//設置分離屬性
pthread_attr_t attr 創(chuàng)建一個線程屬性結(jié)構(gòu)體變量pthread_attr_init(&attr); 初始化線程屬性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 設置線程屬性為 分離態(tài)pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 設置線程屬性 創(chuàng)建為分離態(tài)的新線程pthread_attr_destroy(&attr); 銷毀線程屬性
2.線程屬性初始化和銷毀
注意:應先初始化線程屬性,再 pthread_create 創(chuàng)建線程
初始化線程屬性
int pthread_attr_init(pthread_attr_t *attr);
成功:0;失敗:錯誤號
銷毀線程屬性所占用的資源
int pthread_attr_destroy(pthread_attr_t *attr);
成功:0;失敗:錯誤號
3.線程的分離狀態(tài)
線程的分離狀態(tài)決定一個線程以什么樣的方式來終止自己。
非分離狀態(tài):
線程的默認屬性是非分離狀態(tài),這種情況下,原有的線程等待創(chuàng)建的線程結(jié)束。只有當 pthread_join()
函數(shù)返回時,創(chuàng)建的線程才算終止,才能釋放自己占用的系統(tǒng)資源。
分離狀態(tài):
分離線程沒有被其他的線程所等待,自己運行結(jié)束了,線程也就終止了,馬上釋放系統(tǒng)資源。應該
根據(jù)自己的需要,選擇適當?shù)姆蛛x狀態(tài)。
線程分離狀態(tài)的函數(shù)
設置線程屬性,分離 or 非分離
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
獲取程屬性,分離 or 非分離
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
參數(shù):
attr:已初始化的線程屬性
detachstate: PTHREAD_CREATE_DETACHED(分離線程)
? PTHREAD _CREATE_JOINABLE(非分離線程)
這里要注意的一點是,如果設置一個線程為分離線程,而這個線程運行又非常快,它很可能在 pthread_create函數(shù)返回之前就終止了,它終止以后就可能將線程號和系統(tǒng)資源移交給其他的線程使用,這樣調(diào)用pthread_create的線程就得到了錯誤的線程號。要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創(chuàng)建的線程里調(diào)用 pthread_cond_timedwait 函數(shù),讓這個線程等待一會兒,留出足夠的時間讓函數(shù) pthread_create 返回。設置一段等待時間,是在多線程編程里常用的方法。但是注意不要使用諸如 wait()之類的函數(shù),它們是使整個進程睡眠,并不能解決線程同步的問題。
//設置分離屬性
pthread_attr_t attr 創(chuàng)建一個線程屬性結(jié)構(gòu)體變量pthread_attr_init(&attr); 初始化線程屬性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 設置線程屬性為 分離態(tài)pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 設置線程屬性 創(chuàng)建為分離態(tài)的新線程pthread_attr_destroy(&attr); 銷毀線程屬性
4.線程屬性控制示例
//設置分離屬性
pthread_attr_t attr 創(chuàng)建一個線程屬性結(jié)構(gòu)體變量pthread_attr_init(&attr); 初始化線程屬性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 設置線程屬性為 分離態(tài)pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 設置線程屬性 創(chuàng)建為分離態(tài)的新線程pthread_attr_destroy(&attr); 銷毀線程屬性
#include <pthread.h>#define SIZE 0x100000void *th_fun(void *arg)
{while(1);sleep(1);
}int main(void)
{pthread_t tid;int err, detachstate, i = 1;pthread_attr_t attr;size_t stacksize;void *stackaddr;pthread_attr_init(&attr);pthread_attr_getstack(&attr, &stackaddr, &stacksize);pthread_attr_getdetachstate(&attr, &detachstate);if (detachstate == PTHREAD_CREATE_DETACHED)printf("thread detached\n");else if (detachstate == PTHREAD_CREATE_JOINABLE)printf("thread join\n");elseprintf("thread unknown\n");pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);while (1) {stackaddr = malloc(SIZE);if (stackaddr == NULL) {perror("malloc");exit(1);}stacksize = SIZE;pthread_attr_setstack(&attr, stackaddr, stacksize);err = pthread_create(&tid, &attr, th_fun, NULL);if (err != 0) {printf("%s\n", strerror(err));exit(1);}printf("%d\n", i++);}pthread_attr_destroy(&attr);return 0;
}
5.線程使用注意事項
1.主線程退出其他線程不退出,主線程應調(diào)用pthread_exit
2.避免僵尸線程
-
pthread_join
-
pthread_detach
-
pthread_create 指定分離屬性
被join線程可能在join函數(shù)返回前就釋放完自己的所有內(nèi)存資源,所以不應當返回被回收線程棧中的值;
3.malloc和mmap申請的內(nèi)存可以被其他線程釋放
4.應避免在多線程模型中調(diào)用fork除非,馬上exec,子進程中只有調(diào)用fork的線程存在,其他線程在子進程
中均pthread_exit
5.信號的復雜語義很難和多線程共存,應避免在多線程引入信號機制
以下是線程同步的內(nèi)容:
6.同步概念
同步
所謂同步,即同時起步,協(xié)調(diào)一致。
不同的對象,對“同步”的理解方式略有不同。如,設備同步,是指在兩個設備之間規(guī)定一個共同的時間參考;數(shù)據(jù)庫同步,是指讓兩個或多個數(shù)據(jù)庫內(nèi)容保持一致,或者按需要部分保持一致;文件同步,是指讓兩個或多個文件夾里的文件保持一致。等等。
而,編程中、通信中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協(xié)同、協(xié)助、互相配合。主旨在協(xié)同步調(diào),按預定的先后次序運行。
線程同步
線程同步,指一個線程發(fā)出某一功能調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用不返回。同時其它線程為保證數(shù)據(jù)
一致性,不能調(diào)用該功能。
舉例1:銀行存款 5000。柜臺,折:取3000;提款機,卡:取3000。剩余:2000
舉例2:內(nèi)存中100字節(jié),線程T1欲填入全1,線程T2欲填入全0。但如果T1執(zhí)行了50個字節(jié)失去cpu,T2
執(zhí)行,會將T1寫過的內(nèi)容覆蓋。當T1再次獲得cpu繼續(xù) 從失去cpu的位置向后寫入1,當執(zhí)行結(jié)束,內(nèi)存中的
100字節(jié),既不是全1,也不是全0。
產(chǎn)生的現(xiàn)象叫做“與時間有關的錯誤”(time related)。
為了避免這種數(shù)據(jù)混亂,線程需要同步。
“同步”的目的,是為了避免數(shù)據(jù)混亂,解決與時間有關的錯誤。實際上,不僅線程間需要同步,進程間、信
號間等等都需要同步機制。因此,所有“多個控制流,共同操作一個共享資源”的情況,都需要同步。
7.互斥鎖
1.概述
數(shù)據(jù)混亂原因:
-
資源共享(獨享資源則不會)
-
調(diào)度隨機(意味著數(shù)據(jù)訪問會出現(xiàn)競爭)
-
線程間缺乏必要的同步機制。
以上 3 點中,前兩點不能改變,欲提高效率,傳遞數(shù)據(jù),資源必須共享。只要共享資源,就一定會出現(xiàn)競爭。只要存在競爭關系,數(shù)據(jù)就很容易出現(xiàn)混亂。所以只能從第三點著手解決。使多個線程在訪問共享資源的時候,出現(xiàn)互斥。
互斥鎖(也稱互斥量)用于保護關鍵代碼段,以確保其獨占式的訪問,這有些像二進制信號量(信號量),當進入關鍵代碼段時,我們需要獲得互斥鎖并將其加鎖,這等價于二進制信號量的P操作;當離開關鍵代碼段時,我們需要對互斥鎖解鎖,以喚醒其他等待該互斥鎖的線程,這相當于二進制信號量的V操作。
注意:同一時刻,只能有一個線程持有該鎖
當 A 線程對某個全局變量加鎖訪問,B 在訪問前嘗試加鎖,拿不到鎖,B 阻塞。C 線程不去加鎖,而直接訪問該全局變量,依然能夠訪問,但會出現(xiàn)數(shù)據(jù)混亂。
所以,互斥鎖實質(zhì)上是操作系統(tǒng)提供的一把“建議鎖”(又稱“協(xié)同鎖”),建議程序中有多線程訪問共享資源的時候使用該機制。但,并沒有強制限定。
因此,即使有了 mutex,如果有線程不按規(guī)則來訪問數(shù)據(jù),依然會造成數(shù)據(jù)混亂。
2.互斥鎖基礎 API
#include <pthread.h>
int pthread_mutex_init ( pthread_mutex_t* mutex, /* 初始化互斥鎖 */const pthread_mutexattr_t* mutexattr );
int pthread_mutex_destory ( pthread_mutex_t* mutex ); /* 銷毀互斥鎖 */
int pthread_mutex_lock ( pthread_mutex_t* mutex ); /* 以原子操作的方式給一個互斥鎖加鎖 */
int pthread_mutex_trylock ( pthread_mutex_t* mutex ); /* 相當于 pthread_mutex_lock 的非阻塞版本 ,非阻塞輪詢*/
int pthread_mutex_unlock ( pthread_mutex_t* mutex ); /* 以原子操作的方式給一個互斥鎖解鎖 */
以上5個函數(shù)的返回值都是:成功返回0,失敗返回錯誤號。
pthread_mutex_t 類型,其本質(zhì)是一個結(jié)構(gòu)體。為簡化理解,應用時可忽略其實現(xiàn)細節(jié),簡單當成整數(shù)看待。
pthread_mutex_t mutex;
變量 mutex只有兩種取值1、0
使用mutex(互斥量、互斥鎖)一般步驟:
pthread_mutex_t 類型。 1. pthread_mutex_t lock; 創(chuàng)建鎖2 pthread_mutex_init; 初始化 13. pthread_mutex_lock;加鎖 1-- --> 04. 訪問共享數(shù)據(jù)(stdout) 5. pthrad_mutext_unlock();解鎖 0++ --> 16. pthead_mutex_destroy;銷毀鎖
1.初始化和銷毀
int pthread_mutex_init ( pthread_mutex_t* mutex, /* 初始化互斥鎖 */const pthread_mutexattr_t* mutexattr );
參數(shù):
mutex:咱們創(chuàng)建的鎖
mutexattr:鎖的屬性
int pthread_mutex_destory ( pthread_mutex_t* mutex ); /* 銷毀互斥鎖 */
參數(shù)同上
restrict關鍵字,用來限定指針變量。被該關鍵字限定的指針變量所指向的內(nèi)存操作,必須由本指針完成。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); //動態(tài)初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //靜態(tài)初始化。
2.加鎖解鎖
int pthread_mutex_lock ( pthread_mutex_t* mutex ); /* 以原子操作的方式給一個互斥鎖加鎖 */
int pthread_mutex_trylock ( pthread_mutex_t* mutex ); /* 相當于 pthread_mutex_lock 的非阻塞版本 ,非阻塞輪詢*/
int pthread_mutex_unlock ( pthread_mutex_t* mutex ); /* 以原子操作的方式給一個互斥鎖解鎖 */
參數(shù)都是我們創(chuàng)建的鎖
3.使用案例
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>pthread_mutex_t mutex;void err_thread(int ret, char *str)
{if (ret != 0) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);}
}void *tfn(void *arg)
{srand(time(NULL));while (1) {pthread_mutex_lock(&mutex);printf("hello ");sleep(rand() % 3); /*模擬長時間操作共享資源,導致cpu易主,產(chǎn)生與時間有關的錯誤*/printf("world\n");pthread_mutex_unlock(&mutex);sleep(rand() % 3);}return NULL;
}int main(void)
{int flag = 5;pthread_t tid;srand(time(NULL));pthread_mutex_init(&mutex, NULL);pthread_create(&tid, NULL, tfn, NULL);while (flag--) {pthread_mutex_lock(&mutex);printf("HELLO ");sleep(rand() % 3);printf("WORLD\n");pthread_mutex_unlock(&mutex);sleep(rand() % 3);}pthread_cancel(tid); // 將子線程殺死,子線程中自帶取消點pthread_join(tid, NULL);pthread_mutex_destroy(&mutex);return 0; //main中的return可以將整個進程退出
}
4.注意事項
1.盡量保證鎖的粒度, 越小越好。(訪問共享數(shù)據(jù)前,加鎖。訪問結(jié)束立即解鎖。)
2.互斥鎖,本質(zhì)是結(jié)構(gòu)體。 我們可以看成整數(shù)。 可以認為是初值為 1。(pthread_mutex_init() 函數(shù)調(diào)用成功。)
3.加鎖: --操作, 阻塞線程。
4.解鎖: ++操作, 換醒阻塞在鎖上的線程。
5.try鎖
int pthread_mutex_trylock ( pthread_mutex_t* mutex ); /* 相當于 pthread_mutex_lock 的非阻塞版本 ,非阻塞輪詢*/
lock加鎖失敗會阻塞,等待鎖釋放
trylock不斷嘗試加鎖,加鎖失敗直接返回錯誤號(如:EBUSY),不阻塞
3.互斥鎖屬性
pthread_mutex_t
結(jié)構(gòu)體描述互斥鎖的屬性,線程庫提供了一系列函數(shù)來操作pthread_mutexattr_t
類型的變量,以方便我們獲取和設置互斥鎖屬性,以下是其中一些主要的函數(shù):
#include <pthread.h>/* 初始化互斥鎖屬性對象 */
int pthread_mutexattr_init ( pthread_mutexattr_t* attr );/* 銷毀互斥鎖屬性對象 */
inrt pthread_mutexattr_destroy ( pthread_mutexattr_t* attr );/* 獲取和設置互斥鎖的 pshared 屬性 */
int pthread_mutexattr_getpshared ( const pthread_mutexattr_t* attr, int* pshared );
int pthread_muextattr_setpshared ( pthread_mutexattr_t* attr, int* pshared );/* 獲取和設置互斥鎖的 type 屬性 */
int pthread_mutexattr_gettype ( const pthread_mutexattr_t* attr, int* type );
int pthread_mutexattr_settype ( pthread_mutexattr_t* attr, int* type );
本書僅討論互斥鎖的兩種常用屬性:pshared
和type
。
互斥鎖屬性pshared
指定是否允許跨進程共享互斥鎖,其可選值為:
PTHREAD_PROCESS_SHARED
:互斥鎖可以被跨進程共享。PTHREAD_PROCESS_PRIVATE
:互斥鎖只能和鎖的初始化線程隸屬于同一個進程的線程共享。
互斥鎖屬性type
指定互斥鎖的類型,Linux支持以下4種互斥鎖:
PTHREAD_MUTEX_NORMAL
:普通鎖,這是互斥鎖的默認類型。當一個線程對一個普通鎖加鎖后,其余請求該鎖的線程將形成一個等待隊列,并在該鎖解鎖后按優(yōu)先級獲得它。這種鎖類型保證了資源分配的公平性,但也容易引發(fā)問題:一個線程如果對一個已經(jīng)加鎖的普通鎖再次加鎖,將引發(fā)死鎖;對一個已經(jīng)被其他線程加鎖的普通鎖解鎖,或者對一個已經(jīng)解鎖的普通鎖再次解鎖,將導致不可預期的后果。PTHREAD_MUTEX_ERRORCHECK
:檢錯鎖。一個線程如果對一個自己加鎖的檢錯鎖再次加鎖,則加鎖操作返回EDEADLK
。對一個已經(jīng)被其他線程加鎖的檢錯鎖解鎖,或?qū)σ粋€已經(jīng)解鎖的檢錯鎖再次解鎖,則解鎖操作返回EPERM
。PTHREAD_MUTEX_RECURSIVE
:嵌套鎖。這種鎖允許一個線程在釋放鎖前多次對它加鎖而不發(fā)生死鎖,但如果其他線程要獲得這個鎖,則當前鎖的擁有者必須執(zhí)行相應次數(shù)的解鎖操作。對一個已經(jīng)被其他線程加鎖的嵌套鎖解鎖,或?qū)σ粋€已經(jīng)解鎖的嵌套鎖再次解鎖,則解鎖操作將返回EPERM
。PTHREAD_MUTEX_DEFAULT
:默認鎖。通常被映射為以上三種鎖之一。
4.死鎖
是使用鎖不恰當造成的現(xiàn)象:
-
線程試圖對同一個互斥量A加鎖兩次。
-
線程1擁有A鎖,請求獲得B鎖;線程2擁有B鎖,請求獲得A鎖
死鎖使一個或多個線程被掛起而無法繼續(xù)執(zhí)行,且這種情況還不容易被發(fā)現(xiàn)。
在一個線程中對一個已經(jīng)加鎖的普通鎖再次加鎖將導致死鎖。另外,如果兩個線程按照不同順序來申請兩個互斥鎖,也容易產(chǎn)生死鎖,如以下代碼所示:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;void *another (void *arg) {pthread_mutex_lock(&mutex_b); /* 子線程上鎖 mutex_b */printf("in child thread, got mutex b, waiting for mutex a\n");sleep(5);++b;pthread_mutex_lock(&mutex_a); /* 子線程上鎖 mutex_a */b += a++;pthread_mutex_unlock(&mutex_a); /* 解鎖 */pthread_mutex_unlock(&mutex_b);pthread_exit(NULL);
}int main () {pthread_t id;pthread_mutex_init(&mutex_a, NULL); /* 初始化互斥鎖 */pthread_mutex_init(&mutex_b, NULL);pthread_create(&id, NULL, another, NULL); /* 創(chuàng)建線程 */pthread_mutex_lock(&mutex_a); /* 主線程上鎖 mutex_a */printf("in parent thread, got mutex a, waiting for mutex b\n");sleep(5);++a;pthread_mutex_lock(&mutex_b); /* 主線程上鎖 mutex_b */a += b++;pthread_mutex_unlock(&mutex_b);pthread_mutex_unlock(&mutex_a);/* 主線程等待子線程結(jié)束,然后銷毀互斥鎖以釋放資源 */pthread_join(id, NULL);pthread_mutex_destroy(&mutex_a);pthread_mutex_destroy(&mutex_b);return 0;
}
由于兩個線程都在等待對方已經(jīng)持有的鎖釋放,因此會發(fā)生死鎖,兩個線程都將永遠等待下去。 為了避免死鎖,應確保所有線程以相同的順序獲取互斥鎖。
編譯:-lpthread
選項確保鏈接了 POSIX 線程庫。
g++ -o test test.cpp -lpthread
8.讀寫鎖
1.原理
1.鎖只有一把。以讀方式給數(shù)據(jù)加鎖,那鎖就是讀鎖,以寫方式給數(shù)據(jù)加鎖,那鎖就是寫鎖。
2.讀共享,寫獨占。
3.寫鎖優(yōu)先級高。
-
如果有五個進程同時請求鎖,1個寫請求4個讀請求,那么優(yōu)先給寫鎖。
-
如果4個讀請求比寫請求先到,并且已經(jīng)加鎖成功,那么不會斷開讀請求的進程給寫請求的進程鎖的。
-
**如果讀鎖和寫鎖在同一隊列阻塞等待,那么優(yōu)先給寫鎖:**如果進程1是讀進程已經(jīng)加鎖成功在讀了,后邊同時來了3個進程,2,4進程寫請求,3進程讀請求,這個時候是這樣的:1讀完之后,2和4寫,寫完之后3再讀(寫優(yōu)先級高)
2.特性
-
讀寫鎖是“寫模式加鎖”時,解鎖前,所有對該鎖加鎖的線程都會被阻塞。
-
讀寫鎖是“讀模式加鎖”時,
-
讀寫鎖是“讀模式加鎖”時,既有試圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程。那么讀寫鎖
會阻塞隨后的讀模式鎖請求。優(yōu)先滿足寫模式鎖。讀鎖、寫鎖并行阻塞,寫鎖優(yōu)先級高
**讀寫鎖也叫共享-獨占鎖。**當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨
占模式鎖住的。寫獨占、讀共享。
讀寫鎖非常適合于對數(shù)據(jù)結(jié)構(gòu)讀的次數(shù)遠大于寫的情況。
如果線程以讀模式對其加鎖會成功;如果線程以寫模式加鎖會阻塞。
相較于互斥量而言,當讀線程多的時候,提高訪問效率
3.對應函數(shù)
pthread_rwlock_t rwlock;pthread_rwlock_init(&rwlock, NULL);pthread_rwlock_rdlock(&rwlock);
pthread_rwlock_wrlock(&rwlock); pthread_rwlock_tryrdlock(&rwlock);
pthread_rwlock_trywrlock(&rwlock); pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
以上都是成功返回0失敗返回錯誤號
pthread_rwlock_t類型 用于定義一個讀寫鎖變量。
pthread rwlock t rwlock;
1.初始化和銷毀
pthread_rwlock_init 函數(shù)
初始化一把讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
參 2:attr 表讀寫鎖屬性,通常使用默認屬性,傳 NULL 即可。
pthread_rwlock_destroy 函數(shù)
銷毀一把讀寫鎖
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
2.加鎖解鎖
加鎖
pthread_rwlock_rdlock 函數(shù)
以讀方式請求讀寫鎖。(常簡稱為:請求讀鎖)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock 函數(shù)
以寫方式請求讀寫鎖。(常簡稱為:請求寫鎖)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock 函數(shù)
非阻塞以讀方式請求讀寫鎖(非阻塞請求讀鎖)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock 函數(shù)
非阻塞以寫方式請求讀寫鎖(非阻塞請求寫鎖)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
解鎖
pthread_rwlock_unlock 函數(shù)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
4.示例
/* 3個線程不定時 "寫" 全局資源,5個線程不定時 "讀" 同一全局資源 */#include <stdio.h>
#include <unistd.h>
#include <pthread.h>int counter; //全局資源
pthread_rwlock_t rwlock;void *th_write(void *arg)
{int t;int i = (int)arg;while (1) {t = counter; // 保存寫之前的值usleep(1000);pthread_rwlock_wrlock(&rwlock);printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);pthread_rwlock_unlock(&rwlock);usleep(9000); // 給 r 鎖提供機會}return NULL;
}void *th_read(void *arg)
{int i = (int)arg;while (1) {pthread_rwlock_rdlock(&rwlock);printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);pthread_rwlock_unlock(&rwlock);usleep(2000); // 給寫鎖提供機會}return NULL;
}int main(void)
{int i;pthread_t tid[8];pthread_rwlock_init(&rwlock, NULL);for (i = 0; i < 3; i++)pthread_create(&tid[i], NULL, th_write, (void *)i);for (i = 0; i < 5; i++)pthread_create(&tid[i+3], NULL, th_read, (void *)i);for (i = 0; i < 8; i++)pthread_join(tid[i], NULL);pthread_rwlock_destroy(&rwlock); //釋放讀寫瑣return 0;
}
9.條件變量
如果說互斥鎖是用于同步線程對共享數(shù)據(jù)的訪問,那么條件變量則是用于在線程之間同步共享數(shù)據(jù)的值。條件變量提供了一種線程間的通知機制:當某個共享數(shù)據(jù)達到某個值時,喚醒等待這個共享數(shù)據(jù)的線程。
條件變量是多線程編程中用于同步的一種機制,它允許線程在某些條件未被滿足時暫停執(zhí)行,并在條件滿足時被喚醒繼續(xù)執(zhí)行。條件變量通常與互斥鎖(mutexes)一起使用,以協(xié)調(diào)對共享資源的訪問。這是一種避免忙等(busy-waiting)并減少CPU資源浪費的有效方式。
1.工作原理
- 等待條件變量:當線程需要訪問某個共享資源,但條件不滿足時,它會通過互斥鎖保護條件變量,并在該條件變量上等待。在這個等待過程中,線程會釋放互斥鎖,以便其他線程可以修改這個條件。
- 喚醒等待的線程:其他線程在修改了條件之后,可以通過條件變量來喚醒一個或多個正在等待這個條件的線程。
- 重新檢查條件:被喚醒的線程會重新獲取互斥鎖,并再次檢查條件是否滿足。如果條件滿足,線程繼續(xù)執(zhí)行;如果不滿足,線程可能會再次等待。
2.對應函數(shù)
1.總覽
條件變量的相關函數(shù)如下:
#incldue <pthread.h>/* 初始化條件變量 */
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);/* 銷毀條件變量 */
int pthread_cond_destroy (pthread_cond_t* cond);/* 以廣播方式喚醒所有等待目標條件的線程 */
int pthread_cond_broadcast (pthread_cond_t* cond);/* 喚醒一個等待目標條件變量的線程,喚醒哪個線程取決于線程的優(yōu)先級和調(diào)度策略 */
int pthread_cond_signal (pthread_cond_t* cond);/* 等待目標條件變量 */
int pthread_cond_wait (ptread_cond_t* cond, pthread_mutex_t* mutex);/*限時等待一個條件變量*/
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
pthread_cond_t 類型 用于定義條件變量
pthread_cond_t cond;
成功返回0失敗直接返回錯誤號
pthread_cond_signal(): 喚醒阻塞在條件變量上的 (至少)一個線程。
pthread_cond_broadcast(): 喚醒阻塞在條件變量上的 所有線程。
2.創(chuàng)建和銷毀
創(chuàng)建
/* 初始化條件變量 */
int pthread_cond_init (pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
參 2:attr 表條件變量屬性,通常為默認值,傳 NULL 即可
也可以使用靜態(tài)初始化的方法,初始化條件變量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
銷毀
/* 銷毀條件變量 */
int pthread_cond_destroy (pthread_cond_t* cond);
3.wait函數(shù)
阻塞等待一個條件變量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
參數(shù)
cond:條件變量
mutex:互斥鎖
函數(shù)作用:
-
阻塞等待條件變量 cond(參 1)滿足
-
釋放已掌握的互斥鎖(解鎖互斥量)相當于 pthread_mutex_unlock(&mutex);
? **1.2.**兩步為一個原子操作。
- 當被喚醒,pthread_cond_wait 函數(shù)返回時,解除阻塞并重新申請獲取互斥鎖 pthread_mutex_lock(&mutex);
這張圖是對wait過程的說明:
1.鎖是提前創(chuàng)建和初始化好的,然后加鎖
2.調(diào)用pthread_cond_wait,看條件變量是否滿足
3.不滿足就阻塞等待,阻塞等待的時候就把鎖給解了,讓別人用去了(判斷阻塞和解鎖這兩步是一個原子操作)
4.等到滿足條件變量滿足的時候,再申請重新加鎖(重新加鎖是條件變量內(nèi)部實現(xiàn),不需要咱們自己加鎖)
4.pthread_cond_timedwait 函數(shù)
限時等待一個條件變量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
參 3:
參看 man sem_timedwait 函數(shù),查看 struct timespec 結(jié)構(gòu)體。
struct timespec {
time_t tv_sec;
/* seconds */ 秒
long tv_nsec;
/* nanosecondes*/ 納秒
}
形參 abstime:絕對時間。
如:time(NULL)返回的就是絕對時間。而 alarm(1)是相對時間,相對當前時間定時 1 秒鐘。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定時到 1970 年 1 月 1 日 00:00:01 秒(早已經(jīng)過去)
正確用法:
time_t cur = time(NULL); //獲取當前時間。struct timespec t; //定義 timespec 結(jié)構(gòu)體變量 tt.tv_sec = cur+1; //定時 1 秒pthread_cond_timedwait (&cond, &mutex, &t); //傳參
在講解 setitimer 函數(shù)時我們還提到另外一種時間類型:
struct timeval {
time_t tv_sec; /* seconds */ 秒
susecods_t tv_usec; /* microseconds */ 微秒
};
3.使用條件變量模擬實現(xiàn)生產(chǎn)者—消費者問題
流程
完整代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>void err_thread(int ret, char *str)
{if (ret != 0) {fprintf(stderr, "%s:%s\n", str, strerror(ret));pthread_exit(NULL);}
}//魔方公共區(qū)域的鏈表
struct msg {int num;struct msg *next;
};struct msg *head;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定義/初始化一個互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 定義/初始化一個條件變量void *produser(void *arg)
{while (1) {struct msg *mp = malloc(sizeof(struct msg));mp->num = rand() % 1000 + 1; // 模擬生產(chǎn)一個數(shù)據(jù)`printf("--produce %d\n", mp->num);pthread_mutex_lock(&mutex); // 加鎖 互斥量mp->next = head; // 寫公共區(qū)域head = mp;pthread_mutex_unlock(&mutex); // 解鎖 互斥量pthread_cond_signal(&has_data); // 喚醒阻塞在條件變量 has_data上的線程.sleep(rand() % 3);}return NULL;
}void *consumer(void *arg)
{while (1) {struct msg *mp;pthread_mutex_lock(&mutex); // 加鎖 互斥量while (head == NULL) {pthread_cond_wait(&has_data, &mutex); // 阻塞等待條件變量, 解鎖} // pthread_cond_wait 返回時, 重新加鎖 mutexmp = head;head = mp->next;pthread_mutex_unlock(&mutex); // 解鎖 互斥量printf("---------consumer id: %lu :%d\n", pthread_self(), mp->num);free(mp);sleep(rand()%3);}return NULL;
}int main(int argc, char *argv[])
{int ret;pthread_t pid, cid;srand(time(NULL));ret = pthread_create(&pid, NULL, produser, NULL); // 生產(chǎn)者if (ret != 0) err_thread(ret, "pthread_create produser error");ret = pthread_create(&cid, NULL, consumer, NULL); // 消費者if (ret != 0) err_thread(ret, "pthread_create consuer error");ret = pthread_create(&cid, NULL, consumer, NULL); // 消費者if (ret != 0) err_thread(ret, "pthread_create consuer error");ret = pthread_create(&cid, NULL, consumer, NULL); // 消費者if (ret != 0) err_thread(ret, "pthread_create consuer error");pthread_join(pid, NULL);pthread_join(cid, NULL);return 0;
}
在公共區(qū)域為空的時候,消費者都會阻塞等待hasdata這個條件變量,都會把鎖釋放掉,而生產(chǎn)者這時會拿到鎖進行生產(chǎn),生產(chǎn)者生產(chǎn)完喚醒消費者進行消費。
4.條件變量優(yōu)勢
相較于 mutex 而言,條件變量可以減少競爭。
如直接使用 mutex,除了生產(chǎn)者、消費者之間要競爭互斥量以外,消費者之間也需要競爭互斥量,但如果匯(鏈表)中沒有數(shù)據(jù),消費者之間競爭互斥鎖是無意義的。有了條件變量機制以后,只有生產(chǎn)者完成生產(chǎn),才會引起消費者之間的競爭。提高了程序效率。
9.POSIX 信號量
1.概述
在Linux上,信號量API有兩組,一組是System V IPC信號量(信號量),另一組是我們要討論的POSIX信號量。這兩組接口很相似,且語義完全相同,但不保證能互換。
進化版的互斥鎖(1 --> N)
由于互斥鎖的粒度比較大,如果我們希望在多個線程間對某一對象的部分數(shù)據(jù)進行共享,使用互斥鎖是沒有辦
法實現(xiàn)的,只能將整個數(shù)據(jù)對象鎖住。這樣雖然達到了多線程操作共享數(shù)據(jù)時保證數(shù)據(jù)正確性的目的,卻無形中導
致線程的并發(fā)性下降。線程從并行執(zhí)行,變成了串行執(zhí)行。與直接使用單進程無異。
信號量,是相對折中的一種處理方式,既能保證同步,數(shù)據(jù)不混亂,又能提高線程并發(fā)
POSIX信號量函數(shù)的名字都以sem_
開頭,不像大多線程函數(shù)那樣以pthread_
開頭。常用的POSIX信號量函數(shù)如下:
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value); /* 初始化一個未命名信號量 */
int sem_destory(sem_t* sem); /* 銷毀信號量 */
int sem_wait(sem_t* sem); /* 以原子操作的方式將信號量的值減1 */
int sem_trywait(sem_t *sem); /* 相當于sem_wait函數(shù)的非阻塞版本 */
int sem_post(sem_t *sem); /* 以原子操作的方式將信號量的值加1 */
上圖中函數(shù)的第一個參數(shù)sem
指向被操作的信號量。
sem_init
函數(shù)用于初始化一個未命名信號量(POSIX信號量API支持命名信號量,但本書不討論)。pshared
參數(shù)指定信號量類型,如果值為0,就表示這個信號量是當前進程的局部信號量,否則該信號量可以在多個進程間共享。value
參數(shù)指定信號量的初始值。初始化一個已經(jīng)被初始化的信號量將導致不可預期的結(jié)果。sem_destroy
函數(shù)用于銷毀信號量,以釋放其占用的內(nèi)核資源,銷毀一個正被其他線程等待的信號量將導致不可預期的結(jié)果。sem_wait
函數(shù)以原子操作的方式將信號量的值減1,如果信號量的值為0,則sem_wait
函數(shù)將被阻塞,直到這個信號量具有非0值。sem_trywait
函數(shù)與sem_wait
函數(shù)類似,但它始終立即返回,而不論被操作的信號量是否具有非0值,相當于sem_wait
函數(shù)的非阻塞版本。當信號量非0時,sem_trywait
函數(shù)對信號量執(zhí)行減1操作,當信號量的值為0時,該函數(shù)返回-1并設置errno為EAGAIN
。sem_post
函數(shù)以原子操作的方式將信號量的值加1,當信號量的值從0變?yōu)?時,其他正在調(diào)用sem_wait
等待信號量的線程將被喚醒。
上圖中的函數(shù)成功時返回0,失敗則返回-1并設置errno。
2.對應函數(shù)
1.總覽
#include<semaphore.h>
sem_t sem;
sem_init 函數(shù)
sem_destroy 函數(shù)
sem_wait 函數(shù)
sem_trywait 函數(shù)
sem_timedwait 函數(shù)
sem_post 函數(shù)
以上 6 個函數(shù)的返回值都是:成功返回 0, 失敗返回-1,同時設置 errno。(注意,它們沒有 pthread 前綴)
sem_t 類型,本質(zhì)仍是結(jié)構(gòu)體。但應用期間可簡單看作為整數(shù),忽略實現(xiàn)細節(jié)(類似于使用文件描述符)。
規(guī)定信號量 sem 不能 < 0。
信號量基本操作:
sem_wait:
1.信號量大于 0,則信號量-- (類比 pthread_mutex_lock)
2.信號量等于 0,造成線程阻塞
對應
sem_post:
將信號量++,同時喚醒阻塞在信號量上的線程 (類比 pthread_mutex_unlock)
但,由于 sem_t 的實現(xiàn)對用戶隱藏,所以所謂的++、–操作只能通過函數(shù)來實現(xiàn),而不能直接++、–符號。
信號量的初值,決定了占用信號量的線程的個數(shù)。
2.初始化和銷毀
初始化一個信號量
int sem_init(sem_t *sem, int pshared, unsigned int value);
參 1:sem 信號量
參 2:pshared 取 0 用于線程間;取非 0(一般為 1)用于進程間
參 3:value 指定信號量初值
sem_destroy 函數(shù)
銷毀一個信號量
int sem_destroy(sem_t *sem);
3.PV操作主要函數(shù)
sem_wait 函數(shù)
給信號量加鎖 –
int sem_wait(sem_t *sem);
sem_post 函數(shù)
給信號量解鎖 ++
int sem_post(sem_t *sem);
sem_trywait 函數(shù)
嘗試對信號量加鎖 – (與 sem_wait 的區(qū)別類比 lock 和 trylock)
int sem_trywait(sem_t *sem);
sem_timedwait 函數(shù)
限時嘗試對信號量加鎖 –
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
參 2:abs_timeout 采用的是絕對時間。
定時 1 秒:
time_t cur = time(NULL); 獲取當前時間。
struct timespec t; 定義 timespec 結(jié)構(gòu)體變量 t
t.tv_sec = cur+1; 定時 1 秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 傳參
3.實現(xiàn)生產(chǎn)者消費者
流程
完整代碼
/*信號量實現(xiàn) 生產(chǎn)者 消費者問題*/#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>#define NUM 5 int queue[NUM]; //全局數(shù)組實現(xiàn)環(huán)形隊列
sem_t blank_number, product_number; //空格子信號量, 產(chǎn)品信號量void *producer(void *arg)
{int i = 0;while (1) {sem_wait(&blank_number); //生產(chǎn)者將空格子數(shù)--,為0則阻塞等待queue[i] = rand() % 1000 + 1; //生產(chǎn)一個產(chǎn)品printf("----Produce---%d\n", queue[i]); sem_post(&product_number); //將產(chǎn)品數(shù)++i = (i+1) % NUM; //借助下標實現(xiàn)環(huán)形sleep(rand()%1);}
}void *consumer(void *arg)
{int i = 0;while (1) {sem_wait(&product_number); //消費者將產(chǎn)品數(shù)--,為0則阻塞等待printf("-Consume---%d\n", queue[i]);queue[i] = 0; //消費一個產(chǎn)品 sem_post(&blank_number); //消費掉以后,將空格子數(shù)++i = (i+1) % NUM;sleep(rand()%3);}
}int main(int argc, char *argv[])
{pthread_t pid, cid;sem_init(&blank_number, 0, NUM); //初始化空格子信號量為5, 線程間共享 -- 0sem_init(&product_number, 0, 0); //產(chǎn)品數(shù)為0pthread_create(&pid, NULL, producer, NULL);pthread_create(&cid, NULL, consumer, NULL);pthread_join(pid, NULL);pthread_join(cid, NULL);sem_destroy(&blank_number);sem_destroy(&product_number);return 0;
}
10.線程同步機制包裝類
為了充分復用代碼,同時后文需要,我們將前面討論的三種線程同步機制分別封裝為三個類,實現(xiàn)在locker.h
頭文件中:
#ifndef LOCKER_H
#define LOCKER_H#include <exception>
#include <pthread.h>
#include <semaphore.h>// 封裝信號量的類
class sem {
public:// 創(chuàng)建并初始化信號量sem() {if (sem_init(&m_sem, 0, 0) != 0) {// 構(gòu)造函數(shù)沒有返回值,可通過拋出異常來報告錯誤throw std::exception();}}// 銷毀信號量~sem() {sem_destroy(&m_sem);}// 等待信號量bool wait() {return sem_wait(&m_sem) == 0;}// 增加信號量bool post() {return sem_post(&m_sem) == 0;}private:sem_t m_sem;
};// 封裝互斥鎖的類
class locker {
public:// 創(chuàng)建并初始化互斥鎖locker() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw std::exception();}}// 銷毀互斥鎖~locker() {pthread_mutex_destroy(&m_mutex);}// 獲取互斥鎖bool lock() {return pthread_mutex_lock(&m_mutex) == 0;}// 釋放互斥鎖bool unlock() {return pthread_mutex_unlock(&m_mutex) == 0;}private:pthread_mutex_t m_mutex;
};// 封裝條件變量的類
class cond {
public:// 創(chuàng)建并初始化條件變量cond() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw std::exception();}if (pthread_cond_init(&m_cond, NULL) != 0) {// 構(gòu)造函數(shù)中一旦出現(xiàn)問題,就應立即釋放已經(jīng)成功分配的資源pthread_mutex_destroy(&m_mutex);throw std::exception();}}// 銷毀條件變量~cond() {pthread_mutex_destroy(&m_mutex);pthread_cond_destroy(&m_cond);}// 等待條件變量bool wait() {int ret = 0;// 作者在此處對互斥鎖加鎖,保護了什么?這導致其他人無法使用該封裝類pthread_mutex_lock(&m_mutex);ret = pthread_cond_wait(&m_cond, &m_mutex);pthread_mutex_unlock(&m_mutex);return ret == 0;}// 喚醒等待條件變量的線程bool signal() {return pthread_cond_signal(&m_cond) == 0;}private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;
};#endif
11.多線程環(huán)境
1.可重入函數(shù)
如果一個函數(shù)能被多個線程同時調(diào)用且不發(fā)生競態(tài)條件,則我們稱它是線程安全的(thread safe),或者說它是可重入函數(shù)。Linux庫函數(shù)只有一小部分是不可重入的。這些庫函數(shù)之所以不可重入,主要是因為其內(nèi)部使用了靜態(tài)變量,但Linux對很多不可重入的庫函數(shù)提供了對應的可重入版本,這些可重入版本的函數(shù)名是在原函數(shù)名尾部加上_r
,如localtime
函數(shù)對應的可重入函數(shù)是localtime_r
。
在多線程程序中調(diào)用庫函數(shù),一定要使用其可重入版本,否則可能導致預想不到的結(jié)果。
2.進程和線程
多線程環(huán)境中,使用fork
調(diào)用產(chǎn)生的死鎖問題
如果多線程的某個線程(可以理解為一個進程)調(diào)用了fork
函數(shù),那么新創(chuàng)建的子進程只擁有一個執(zhí)行線程,它是調(diào)用fork
的那個線程的完整復制,且子進程將自動繼承父進程中互斥鎖、條件變量的狀態(tài),即父進程中已被加鎖的互斥鎖在子進程中也是被鎖住的,這就引起了一個問題:子進程可能不清楚從父進程繼承而來的互斥鎖的具體狀態(tài)(是加鎖還是解鎖狀態(tài)),這個互斥鎖可能被加鎖了,但不是由調(diào)用fork
的線程鎖住的,而是由其他線程鎖住的,此時,子進程若再次對該互斥鎖加鎖會導致死鎖,如以下代碼所示:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>pthread_mutex_t mutex;/* 子線程運行的函數(shù),它首先獲得互斥鎖 mutex ,然后暫停5s,再釋放該互斥鎖 */
void *another(void *arg) {printf("in child thread, lock the mutex\n");pthread_mutex_lock(&mutex);sleep(5);pthread_mutex_unlock(&mutex);
}int main() {pthread_mutex_init(&mutex, NULL);pthread_t id;pthread_create(&id, NULL, another, NULL);/* 父進程中的主線程暫停1s,以確保在執(zhí)行 fork 前,子線程已經(jīng)開始運行并獲得了互斥量 mutex */sleep(1);int pid = fork();if (pid < 0) {pthread_join(id, NULL);pthread_mutex_destroy(&mutex);return 1;} else if (pid == 0) {printf("I am in the child, want to get the lock\n");/* 子進程從父進程繼承了互斥鎖 mutex 的狀態(tài),該互斥鎖處于鎖住的狀態(tài) *//* 這是由父進程中的子線程執(zhí)行 pthread_mutex_lock 引起的,因此以下加鎖操作會一直阻塞 *//* 盡管從邏輯上來說它是不應該阻塞的 */pthread_mutex_lock(&mutex);printf("I can not run to here, oop...\n");pthread_mutex_unlock(&mutex);exit(0);} else {wait(NULL);}pthread_join(id, NULL);pthread_mutex_destroy(&mutex);return 0;
}
關鍵點
- 子線程加鎖:在主線程(進程)中創(chuàng)建的子線程首先獲取互斥鎖并休眠5秒。
fork
調(diào)用:在子線程獲取互斥鎖后,主線程休眠1秒以確保子線程鎖定互斥鎖,然后調(diào)用fork
。fork
之后,父進程和子進程都有一個拷貝的互斥鎖狀態(tài)。- 子進程中的鎖行為:由于
fork
后子進程繼承了互斥鎖的狀態(tài),如果該鎖被鎖定,子進程中的互斥鎖也將處于鎖定狀態(tài)。不同的是,子進程中并沒有線程擁有這個鎖(因為鎖的擁有者是父進程的一個線程),因此嘗試獲取這個鎖將會導致子進程永久阻塞。
效果:子進程被阻塞。
不過,pthread
提供了一個專門的函數(shù)pthread_atfork
,以確保fork
調(diào)用后父進程和子進程都擁有一個清楚的鎖狀態(tài):
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child) (void));
pthread_atfork
函數(shù)將建立3個fork
句柄幫助我們清理互斥鎖的狀態(tài)。
prepare
句柄將在fork
函數(shù)創(chuàng)建出子進程前被執(zhí)行,它可以用來鎖住父進程中的互斥鎖。parent
句柄是fork
函數(shù)創(chuàng)建出子進程后,fork
函數(shù)返回前,在父進程中被執(zhí)行,它的作用是釋放所有在prepare
句柄中被鎖住的互斥鎖。child
句柄是在fork
函數(shù)返回前,在子進程中執(zhí)行,它和parent
句柄一樣,也是用于釋放所有在prepare
句柄中被鎖住的互斥鎖。
該函數(shù)成功時返回0,失敗則返回錯誤碼。
要讓以上代碼正常工作,需要在fork
調(diào)用前加上以下代碼:
void prepare () {pthread_mutex_lock ( &mutex );
}void infork () {pthread_mutex_unlock ( &mutex );
}pthread_atfork ( prepare, infork, infork );
效果:未發(fā)生死鎖。
人話翻譯一下它這三個函數(shù)在干什么
- 第一段代碼導致死鎖的原因
- 父子進程共享互斥鎖狀態(tài)導致阻塞:
- 在第一段代碼中,首先創(chuàng)建了一個互斥鎖
mutex
,并在子線程中獲取了這個互斥鎖(pthread_mutex_lock(&mutex)
),然后線程休眠 5 秒。接著在父進程中執(zhí)行fork
操作。 - 子進程會繼承父進程中互斥鎖的狀態(tài),此時互斥鎖在子進程中仍然是被鎖定的狀態(tài),因為它是從父進程繼承過來的。當子進程試圖再次獲取已經(jīng)被鎖定的互斥鎖(
pthread_mutex_lock(&mutex)
)時,根據(jù)互斥鎖的默認行為(這里假設是標準互斥鎖PTHREAD_MUTEX_NORMAL
),它會被阻塞,等待鎖被釋放。但是這個鎖不會被釋放,因為在子進程中沒有其他線程可以釋放它(子進程中獲取鎖的代碼還沒執(zhí)行成功),從而導致死鎖。
- 在第一段代碼中,首先創(chuàng)建了一個互斥鎖
- 父子進程共享互斥鎖狀態(tài)導致阻塞:
- 第二段代碼不會死鎖的原因
pthread_atfork
函數(shù)的作用:- 在第二段代碼中,添加了
pthread_atfork
相關的函數(shù)prepare
和infork
。pthread_atfork
函數(shù)的作用是在fork
操作前后對互斥鎖進行特殊處理,以避免父子進程之間由于互斥鎖狀態(tài)不一致而導致的問題。 - 在
fork
操作之前,prepare
函數(shù)會被調(diào)用,它會獲取互斥鎖(pthread_mutex_lock(&mutex)
)。這里需要注意的是,pthread
庫對pthread_atfork
中的prepare
操作可能會有特殊處理,即使互斥鎖已經(jīng)被獲取(被父進程中的子線程獲取),這個操作也不會導致死鎖,而是將互斥鎖狀態(tài)穩(wěn)定在被鎖定狀態(tài)。 - 當
fork
操作完成后,在子進程中infork
函數(shù)會被調(diào)用,它會釋放互斥鎖(pthread_mutex_unlock(&mutex)
)。這樣就使得子進程從一個互斥鎖已經(jīng)被解鎖的狀態(tài)開始,當子進程后續(xù)嘗試獲取互斥鎖(pthread_mutex_lock(&mutex)
)時,就不會像第一段代碼那樣被阻塞,從而避免了死鎖。
- 在第二段代碼中,添加了
- 兩段代碼的主要區(qū)別
- 互斥鎖狀態(tài)處理方式:
- 第一段代碼沒有對
fork
操作前后的互斥鎖狀態(tài)進行特殊處理,子進程繼承了父進程中被鎖定的互斥鎖狀態(tài),并且由于再次嘗試獲取已鎖定的互斥鎖而導致死鎖。 - 第二段代碼通過
pthread_atfork
函數(shù)及其相關的prepare
和infork
函數(shù),在fork
操作前后對互斥鎖狀態(tài)進行了清理和調(diào)整,使得子進程從一個合理的、互斥鎖已解鎖的狀態(tài)開始,避免了子進程因繼承不適當?shù)幕コ怄i狀態(tài)而導致的死鎖問題。
- 第一段代碼沒有對
- 互斥鎖狀態(tài)處理方式:
總結(jié)
就是pthread_atfork函數(shù)在進入子進程前會獲取一下父進程鎖的狀態(tài),不管有沒有被鎖咱給它鎖上,如果被鎖了,我們知道互斥鎖被鎖了以后再次加鎖會導致死鎖,可能它函數(shù)內(nèi)部設計避免了這種情況。然后再把咱鎖上的通通解鎖,再去執(zhí)行子進程代碼。
相當于子進程代碼的初始狀態(tài)就是任何鎖都沒有被加鎖的情況,就避免了死鎖的發(fā)生。
3.線程和信號
每個線程都能獨立設置信號掩碼,進程設置信號掩碼的函數(shù)是sigprocmask
(見信號掩碼),但在多線程環(huán)境下應使用pthread_sigmask
函數(shù)設置信號掩碼:
#include <pthread.h>
#include <signal.h>
int pthread_sigmask ( int how, const sigset_t* newmask, sigset_t* oldmask );
pthread_sigmask
函數(shù)的參數(shù)與sigprocmask
函數(shù)的參數(shù)完全相同。pthread_sigmask
函數(shù)成功時返回0,失敗返回錯誤碼。
由于進程中所有線程共享該進程的信號,所以線程庫將根據(jù)線程掩碼決定把信號發(fā)送給哪個具體的線程。而且,所有線程共享信號處理函數(shù),當我們在一個線程中設置了某個信號的信號處理函數(shù)后,它將覆蓋其他線程為同一信號設置的信號處理函數(shù)。
因此,我們應該定義一個專門的線程來處理所有信號,這可通過以下兩個步驟實現(xiàn):
- 在主線程創(chuàng)建出其他子線程前就調(diào)用
pthread_sigmask
來設置好信號掩碼,所有新創(chuàng)建的子線程將自動繼承這個信號掩碼,這樣,所有線程都不會響應被屏蔽的信號了。 - 在某個線程中調(diào)用以下函數(shù)等待信號并處理:
#include <signal.h>
int sigwait ( const sigset_t* set, int* sig );
set
參數(shù)指定要等待的信號的集合,我們可以將其指定為在第 1 步中創(chuàng)建的信號掩碼,表示在該線程中等待所有被屏蔽的信號。參數(shù)sig
指向的整數(shù)用于存儲該函數(shù)返回的信號值。sigwait
成功時返回0,失敗則返回錯誤碼。一旦sigwait
函數(shù)成功返回,我們就能對收到的信號做處理了,顯然,如果我們使用了sigwait
函數(shù),就不應再為信號設置信號處理函數(shù)了。
pthread
還提供了pthread_kill
函數(shù),使我們可以把信號發(fā)送給指定線程:
#include <signal.h>
int pthread_kill ( pthread_t thread, int sig );
thread
參數(shù)指定目標線程。sig
參數(shù)指定待發(fā)送信號,如果sig
參數(shù)為0,則pthread_kill
不發(fā)送信號,但它仍會進行錯誤檢查。我們可用此方法檢查目標線程是否存在。pthread_kill
函數(shù)成功時返回0,失敗則返回錯誤碼。
在一個線程中統(tǒng)一處理所有信號
以下代碼取自pthread_sigmask
函數(shù)的man手冊,它展示了如何通過以上兩個步驟實現(xiàn)在一個線程中統(tǒng)一處理所有信號:
主線程設置了一個信號掩碼來阻塞特定的信號(在這個例子中是SIGQUIT
和SIGUSR1
),然后創(chuàng)建一個專門的線程來處理這些信號。這種模式是處理多線程環(huán)境中信號的推薦方式,因為它避免了信號處理和線程執(zhí)行之間的競爭條件。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>// perror函數(shù)根據(jù)全局errno值打印其相應的錯誤信息到標準錯誤
#define handle_error_en(en, msg) \ do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)/* 在 sig_thread 函數(shù)中,線程循環(huán)調(diào)用 sigwait 來等待信號 */
static void *sig_thread(void *arg) {sigset_t *set = (sigset_t *)arg;int s, sig;for (; ; ) {// 第二步,調(diào)用sigwait等待信號s = sigwait(set, &sig);if (s != 0) {handle_error_en(s, "sigwait");}printf("Signal handling thread got signal %d\n", sig);}
}int main(int argc, char *argv[]) {printf("The PID of this process is: %d\n", getpid()); /* 獲取進程 PID */pthread_t thread; /* 線程 */sigset_t set; /* 信號集 */int s;/* 第一步,在主線程中設置信號掩碼,信號集set被初始化并添加了SIGQUIT和SIGUSR1信號: */sigemptyset(&set);sigaddset(&set, SIGQUIT);sigaddset(&set, SIGUSR1);/* 使用 pthread_sigmask 來阻塞這些信號 */s = pthread_sigmask(SIG_BLOCK, &set, NULL);if (s != 0) {handle_error_en(s, "pthread_sigmask");}/* 創(chuàng)建處理信號的線程 */s = pthread_create(&thread, NULL, &sig_thread, (void *)&set);if (s != 0) {handle_error_en(s, "thread_create");}pause();
}
運行程序,并在另一個終端中使用kill
命令發(fā)送SIGQUIT
或SIGUSR1
信號到程序。例如:
kill -SIGQUIT [pid]
kill -SIGUSR1 [pid]