廣州手機(jī)網(wǎng)站建設(shè)黑馬程序員培訓(xùn)機(jī)構(gòu)官網(wǎng)
歡迎各位大佬光臨本文章!!!
還請(qǐng)各位大佬提出寶貴的意見,如發(fā)現(xiàn)文章錯(cuò)誤請(qǐng)聯(lián)系冰冰,冰冰一定會(huì)虛心接受,及時(shí)改正。
本系列文章為冰冰學(xué)習(xí)編程的學(xué)習(xí)筆記,如果對(duì)您也有幫助,還請(qǐng)各位大佬、帥哥、美女點(diǎn)點(diǎn)支持,您的每一分關(guān)心都是我堅(jiān)持的動(dòng)力。
我的博客地址:bingbing~bang的博客_CSDN博客
https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.com
https://gitee.com/bingbingsurercool
系列文章推薦
冰冰學(xué)習(xí)筆記:《信號(hào)》
冰冰學(xué)習(xí)筆記:《管道與共享內(nèi)存》
目錄
系列文章推薦
前言
1.Linux的線程概念
2.線程與進(jìn)程的對(duì)比
2.1線程的優(yōu)缺點(diǎn)
2.2線程的異常和用途
2.3進(jìn)程與線程的資源劃分
3.線程控制
4.線程互斥
4.1為什么需要線程互斥
4.2互斥量的函數(shù)接口
4.3深入理解申請(qǐng)和釋放鎖
4.4可重入函數(shù)和線程安全
4.5死鎖概念
5.線程同步
5.1為什么需要線程同步
5.2條件變量函數(shù)
6.POSIX信號(hào)量
6.1信號(hào)量操作函數(shù)
6.2環(huán)形隊(duì)列的生產(chǎn)消費(fèi)模型
前言
????????之前我們學(xué)到過進(jìn)程的概念,進(jìn)程是系統(tǒng)調(diào)度的基本單位,每個(gè)進(jìn)程都有自己獨(dú)特的PCB機(jī)構(gòu)以及自己獨(dú)有的內(nèi)存空間。當(dāng)我們想要其他進(jìn)程去執(zhí)行任務(wù)時(shí),我們可以創(chuàng)建子進(jìn)程去執(zhí)行父進(jìn)程分配給子進(jìn)程的任務(wù),子進(jìn)程與父進(jìn)程的代碼和數(shù)據(jù)雖然相同,但是子進(jìn)程的數(shù)據(jù)是父進(jìn)程的拷貝,子進(jìn)程修改并不影響父進(jìn)程。今天我們說講的線程類似于子進(jìn)程,也是一個(gè)執(zhí)行流,用來執(zhí)行不同任務(wù),但是與進(jìn)程有所區(qū)別。
1.Linux的線程概念
????????在將Linux的線程之前,我們?cè)僦匦抡J(rèn)識(shí)以下進(jìn)程的概念。
????????每一個(gè)進(jìn)程都有自己獨(dú)有的task_struct結(jié)構(gòu)體,結(jié)構(gòu)體中具備該進(jìn)程自己的虛擬空間地址,并通過頁(yè)表映射到物理內(nèi)存中。我們之前并沒有詳細(xì)講解頁(yè)表的存儲(chǔ)方式,頁(yè)表是如何映射這么多的地址空間的呢?
? ? ? ? 如果頁(yè)表一一映射物理內(nèi)存地址,那么頁(yè)表非常大,內(nèi)存根本無法存儲(chǔ)。因此頁(yè)表采用了分成的映射方式。物理內(nèi)存實(shí)際上是按照4kb的單位進(jìn)行劃分的,每一小塊內(nèi)存稱之為頁(yè)框,磁盤上的內(nèi)存也是按照4kb劃分,稱之為頁(yè)幀。頁(yè)表想要映射這些地址,顯然是無法存儲(chǔ)的。
????????頁(yè)表分為兩層進(jìn)行映射,頁(yè)表將4字節(jié)32個(gè)比特位劃分為3組,第一組為前10個(gè)比特位,存儲(chǔ)在一級(jí)頁(yè)表中。中間10個(gè)比特位映射到2級(jí)頁(yè)表中。這樣一個(gè)地址可以根據(jù)前10個(gè)位找到一級(jí)頁(yè)表,通過一級(jí)頁(yè)表找到對(duì)應(yīng)的二級(jí)頁(yè)表,根據(jù)中間10個(gè)比特位就能找到物理內(nèi)存中對(duì)應(yīng)的哪個(gè)4kb空間。最后12個(gè)比特位則是每個(gè)4kb空間的偏移量,通過每個(gè)偏移量則能找到每個(gè)地址。
????????因此CPU在調(diào)度時(shí),找到進(jìn)程的task_struct結(jié)構(gòu)體,通過結(jié)構(gòu)體訪問虛擬地址,然后通過頁(yè)表的映射最終訪問到物理內(nèi)存中的數(shù)據(jù)。而當(dāng)我們創(chuàng)建子進(jìn)程時(shí),子進(jìn)程會(huì)拷貝父進(jìn)程的task_struct,虛擬地址空間,頁(yè)表,并重新映射自己的物理內(nèi)存。CPU調(diào)度子進(jìn)程從而通過映射后找到的是子進(jìn)程對(duì)應(yīng)的物理地址中的數(shù)據(jù)。
????????而此時(shí)我們發(fā)現(xiàn)CPU調(diào)度時(shí)不會(huì)管你的虛擬地址空間是不是自己的,我只需要你的task_struct結(jié)構(gòu)體即可。
????????其實(shí)每一個(gè)進(jìn)程中的task_struct結(jié)構(gòu)體都稱之為一個(gè)線程,即在一個(gè)程序里的一個(gè)執(zhí)行路線就叫做線程(thread)。更準(zhǔn)確的定義是:線程是“一個(gè)進(jìn)程內(nèi)部的控制序列”。因此Linux的線程與進(jìn)程沒有多大的區(qū)別,只不過每個(gè)進(jìn)程有自己獨(dú)有的虛擬地址空間,而多線程則共享一個(gè)進(jìn)程中的虛擬地址空間,即線程在進(jìn)程的地址空間內(nèi)運(yùn)行。
????????所以之前我們學(xué)的進(jìn)程實(shí)際上是內(nèi)部只有一個(gè)執(zhí)行流的進(jìn)程,而內(nèi)部具備多個(gè)執(zhí)行流時(shí),每個(gè)執(zhí)行流就叫做線程。CPU只管調(diào)度task_struct,并不管具備幾個(gè)執(zhí)行流。所以我們看到,線程實(shí)際上才是OS調(diào)度的基本單位。
????????Linux沒有真正意義上的線程結(jié)構(gòu),有的只是輕量級(jí)的進(jìn)程,因此Linux并不能給我們提供線程的相關(guān)接口,只能提供輕量級(jí)的進(jìn)程接口。但是Linux為了方便使用,在用戶層實(shí)現(xiàn)了一套多線程的方案,即pthread庫(kù)。
2.線程與進(jìn)程的對(duì)比
2.1線程的優(yōu)缺點(diǎn)
線程的優(yōu)點(diǎn):
(1)創(chuàng)建一個(gè)新線程的代價(jià)要比創(chuàng)建一個(gè)新進(jìn)程小得多
(2)與進(jìn)程之間的切換相比,線程之間的切換需要操作系統(tǒng)做的工作要少很多。
(3)線程占用的資源要比進(jìn)程少很多
(4)能充分利用多處理器的可并行數(shù)量
(5)在等待慢速I/O操作結(jié)束的同時(shí),程序可執(zhí)行其他的計(jì)算任務(wù)
(6)計(jì)算密集型應(yīng)用,為了能在多處理器系統(tǒng)上運(yùn)行,將計(jì)算分解到多個(gè)線程中實(shí)現(xiàn)
(7)I/O密集型應(yīng)用,為了提高性能,將I/O操作重疊。線程可以同時(shí)等待不同的I/O操作。
線程的缺點(diǎn):
????????線程有可能照成性能損失,如果計(jì)算密集型 線程的數(shù)量比可用的處理器多,那么可能會(huì)有較大的性能損失,這里的性能損失指的是增加了額外的同步和調(diào)度開銷,而可用的資源不變。
????????程序健壯性降低,在一個(gè)多線程程序里,因時(shí)間分配上的細(xì)微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護(hù)的。
????????線程缺乏訪問控制,進(jìn)程是訪問控制的基本粒度,在一個(gè)線程中調(diào)用某些OS函數(shù)會(huì)對(duì)整個(gè)進(jìn)程造成影響。
????????線程的編寫難度提高,編寫與調(diào)試一個(gè)多線程程序比單線程程序困難得多。
2.2線程的異常和用途
????????單個(gè)線程如果出現(xiàn)除零,野指針問題導(dǎo)致線程崩潰,進(jìn)程也會(huì)隨著崩潰。線程是進(jìn)程的執(zhí)行分支,線程出異常,就類似進(jìn)程出異常,進(jìn)而觸發(fā)信號(hào)機(jī)制,終止進(jìn)程,進(jìn)程終止,該進(jìn)程內(nèi)的所有線程也就隨即退出。
? ? ? ? 但是合理的使用多線程,能提高CPU密集型程序的執(zhí)行效率,合理的使用多線程,能提高IO密集型程序的用戶體驗(yàn)。
2.3進(jìn)程與線程的資源劃分
? ? ? ? 進(jìn)程的多個(gè)線程中絕大多數(shù)的資源都是共享的,如代碼段,數(shù)據(jù)段,或者定義的一個(gè)函數(shù)、全局變量,各個(gè)線程都能調(diào)用。線程還共享文件描述符表,每種信號(hào)的處理方式,當(dāng)前工作目錄,用戶id和組id。
????????但是進(jìn)程是資源分配的基本單位,線程是調(diào)度的基本單位,線程也具備自己的數(shù)據(jù),如線程ID,一組寄存器,棧,信號(hào)屏蔽字,errno,調(diào)度優(yōu)先級(jí)。
3.線程控制
????????與線程有關(guān)的函數(shù)構(gòu)成了一個(gè)完整的系列,絕大多數(shù)函數(shù)的名字都是以“pthread_”打頭的。要使用這些函數(shù)庫(kù),要通過引入頭文件<pthread.h>?。鏈接這些線程函數(shù)庫(kù)時(shí)要使用編譯器命令的? ?“-lpthread”選項(xiàng)。
(1)pthread_create:創(chuàng)建新線程
頭文件:#include<pthread>
函數(shù)體:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
參數(shù):thread:返回線程ID
? ? ? ? ? ?attr:設(shè)置線程的屬性,attr為NULL表示使用默認(rèn)屬性
? ? ???????start_routine:是個(gè)函數(shù)地址,線程啟動(dòng)后要執(zhí)行的函數(shù)
? ? ? ? ? ?arg:傳給線程啟動(dòng)函數(shù)的參數(shù)
返回值:成功返回0;失敗返回錯(cuò)誤碼。
注意:
????????傳統(tǒng)的一些函數(shù)是,成功返回0,失敗返回-1,并且對(duì)全局變量errno賦值以指示錯(cuò)誤。
????????pthreads函數(shù)出錯(cuò)時(shí)不會(huì)設(shè)置全局變量errno(而大部分其他POSIX函數(shù)會(huì)這樣做)。而是將錯(cuò)誤代碼通過返回值返回
????????pthreads同樣也提供了線程內(nèi)的errno變量,以支持其它使用errno的代碼。對(duì)于pthreads函數(shù)的錯(cuò)誤, 建議通過返回值業(yè)判定,因?yàn)樽x取返回值要比讀取線程內(nèi)的errno變量的開銷更小
????????下面我們通過代碼創(chuàng)建多個(gè)線程,并驗(yàn)證多線程是否在同一個(gè)進(jìn)程中:
void* handler(void* name)
{const string s=(char*)name;while(true){cout<<s<<"進(jìn)程id為:"<<getpid()<<endl;sleep(1);}
}
int main()
{pthread_t tid[5];char name[64];for(int i=1;i<=5;i++){snprintf(name,sizeof(name),"%s-%d","thread",i);//創(chuàng)建多個(gè)線程pthread_create(&tid[i-1],nullptr,handler,(void*)name);sleep(1);}while(true){cout<<"主線程,pid: "<<getpid()<<endl;sleep(5);}return 0;
}
????????結(jié)果發(fā)現(xiàn),每個(gè)線程的pid與主進(jìn)程的pid完全相同,這意味著線程在進(jìn)程內(nèi)部。?
????????當(dāng)線程出現(xiàn)野指針,除零錯(cuò)誤時(shí),進(jìn)程會(huì)不會(huì)崩潰呢?
void* handler(void* name)
{const string s=(char*)name;int count=0;while(true){cout<<s<<"在運(yùn)行:"<<count<<endl;count++;sleep(1);if(count==5){char* p=nullptr;*p='a';//野指針問題}}
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)"thread-1");while(true){cout<<"主進(jìn)程在運(yùn)行"<<endl;sleep(1);}
}
????????在線程執(zhí)行5秒后,出現(xiàn)野指針錯(cuò)誤,此時(shí)我們發(fā)現(xiàn)一直在運(yùn)行的兩個(gè)線程都會(huì)退出。線程雖然pid相同,但是每個(gè)線程都有自己獨(dú)特的LWP(輕量級(jí)進(jìn)程)號(hào),CPU通過LWP進(jìn)行調(diào)度。
????????而且我們還發(fā)現(xiàn),線程的執(zhí)行并沒有固定的順序,例如在第一個(gè)例子中,線程完全沒有順序,這就說明,線程的運(yùn)行順序和調(diào)度器有關(guān)。線程一旦異常,都可能導(dǎo)致整個(gè)進(jìn)程體系退出,線程在創(chuàng)建并執(zhí)行的時(shí)候線程也是需要等待的,如果不等待也會(huì)出現(xiàn)類似于僵尸進(jìn)程的問題,導(dǎo)致內(nèi)存泄漏。
? ? ? ? 線程之間對(duì)于全局變量也是共享的,一個(gè)線程更改,其他線程的數(shù)據(jù)也會(huì)更改,如果想讓全局變量每個(gè)線程私有,那么需要增加__thread進(jìn)行修飾。
int g_val=0;
void* handler(void* num)
{while(true){cout<<"新線程g_val: "<<g_val++<<endl;sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)"thread-1");while(true){cout<<"主線程g_val: "<<g_val<<endl;sleep(1);}
}
新線程對(duì)g_val進(jìn)行更改,此時(shí)兩個(gè)線程都會(huì)更改:
當(dāng)使用__thread修飾后,新進(jìn)程更改,不影響主線程:
(2)pthread_join:線程等待
頭文件:#include<pthread.h>
函數(shù)體:int pthread_join(pthread_t thread, void **value_ptr);
參數(shù):thread:線程ID
???????????value_ptr:它指向一個(gè)指針,后者指向線程的返回值
返回值:成功返回0;失敗返回錯(cuò)誤碼
????????為什么需要線程等待呢?原因在于已經(jīng)退出的線程,其空間沒有被釋放,仍然在進(jìn)程的地址空間內(nèi)。 創(chuàng)建新的線程不會(huì)復(fù)用剛才退出線程的地址空間。
????????線程在創(chuàng)建后會(huì)去執(zhí)行線程對(duì)應(yīng)的功能函數(shù),該函數(shù)是具備返回值的,那么函數(shù)的返回值返回給誰(shuí)呢?其實(shí)返回值就返回給了創(chuàng)建線程的進(jìn)程,并且通過pthread_join函數(shù)的第二個(gè)參數(shù)獲取。線程等待是默認(rèn)以阻塞的方式進(jìn)行等待,如果線程不退出,就會(huì)一直等待。
用下面的代碼進(jìn)行驗(yàn)證:
void* handler(void* name)
{const string s=(char*)name;int count=0;int* arr=new int[5];while(true){cout<<s<<"在運(yùn)行:"<<count<<endl;arr[count]=count++;if(count==5)break;sleep(1);}return (void*)arr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)"thread-1");int *arr;pthread_join(tid,(void**)&arr);//默認(rèn)阻塞等待cout<<"主線程獲取返回值"<<endl;for(int i=0;i<5;i++){cout<<arr[i]<<" ";}cout<<endl;
}
主線程獲取到返回值,并打印:
????????thread線程以不同的方法終止,通過pthread_join得到的終止?fàn)顟B(tài)是不同的,總結(jié)如下:
????????1. 如果thread線程通過return返回,value_ ptr所指向的單元里存放的是thread線程函數(shù)的返回值。
????????2. 如果thread線程被別的線程調(diào)用pthread_ cancel異常終掉,value_ ptr所指向的單元里存放的是常數(shù) PTHREAD_ CANCELED。
????????3. 如果thread線程是自己調(diào)用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數(shù)。
????????4. 如果對(duì)thread線程的終止?fàn)顟B(tài)不感興趣,可以傳NULL給value_ ptr參數(shù)。
(3)線程終止:pthread_exit
頭文件:#include<pthread.h>
函數(shù)體:void pthread_exit(void *value_ptr);
參數(shù):value_ptr:value_ptr不要指向一個(gè)局部變量。
返回值:無返回值,跟進(jìn)程一樣,線程結(jié)束的時(shí)候無法返回到它的調(diào)用者(自身)
????????線程的終止函數(shù)不能直接調(diào)用exit函數(shù),該函數(shù)意味著進(jìn)程的終止,如果在線程退出時(shí)調(diào)用,整個(gè)進(jìn)程就會(huì)退出。
(4)線程取消:pthread_cancel
頭文件:#include<pthread.h>
函數(shù)體:int pthread_cancel(pthread_t thread);
參數(shù):thread:線程ID
返回值:成功返回0;失敗返回錯(cuò)誤碼
????????線程取消時(shí),一定要主線程取消新線程,并且確保新線程已經(jīng)開始運(yùn)行了。?
void* handler(void* name)
{const string s=(char*)name;int count=0;while(true){cout<<s<<"運(yùn)行中: "<<count<<endl;count++;if(count==5)break;sleep(1);}cout<<"線程終止"<<endl;pthread_exit((void*)2);
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)"thread-1");sleep(3);pthread_cancel(tid);cout<<"3秒后線程取消"<<endl;pthread_join(tid,nullptr);//默認(rèn)阻塞等待
}
(5)獲取線程id: pthread_self
頭文件:#include<pthread.h>
函數(shù)體:pthread_t pthread_self(void);
參數(shù):無參
返回值:返回當(dāng)前線程的線程id
????????對(duì)于Linux目前實(shí)現(xiàn)的NPTL實(shí)現(xiàn)而言,pthread_t類型的線程ID,本質(zhì) 就是一個(gè)進(jìn)程地址空間上的一個(gè)地址。線程之間的棧是不共享的,那么每個(gè)線程的棧是怎么維護(hù)的呢?
? ? ? ? 其實(shí)pthread庫(kù)中給線程維護(hù)了一個(gè)獨(dú)立的??臻g,而該空間的地址就是pthread_t類型的線程id。
?(6)線程分離:pthread_detach
頭文件:#include<pthread.h>
函數(shù)體:int pthread_detach(pthread_t thread);
參數(shù):線程id
返回值:成功返回0,錯(cuò)誤返回錯(cuò)誤碼。
????????默認(rèn)情況下,新創(chuàng)建的線程是joinable的,線程退出后,需要對(duì)其進(jìn)行pthread_join操作,否則無法釋放資源,從而造成系統(tǒng)泄漏。 如果不關(guān)心線程的返回值,join是一種負(fù)擔(dān),這個(gè)時(shí)候,我們可以將線程分離,這就告訴系統(tǒng),當(dāng)線程退出時(shí),自動(dòng)釋放線程資源。
4.線程互斥
4.1為什么需要線程互斥
在了解線程互斥之前,我們先復(fù)習(xí)之前講過的一些概念:
(1)臨界資源:多線程執(zhí)行流共享的資源就叫做臨界資源
(2)臨界區(qū):每個(gè)線程內(nèi)部,訪問臨界資源的代碼,就叫做臨界區(qū)
(3)互斥:任何時(shí)刻,互斥保證有且只有一個(gè)執(zhí)行流進(jìn)入臨界區(qū),訪問臨界資源,通常對(duì)臨界資源起保護(hù)作用
(4)原子性:不會(huì)被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成。
????????線程互斥主要解決的就是線程之間對(duì)臨界資源互相訪問,因?yàn)榫€程調(diào)度時(shí)間不同而造成的數(shù)據(jù)混亂問題。大部分情況,線程使用的數(shù)據(jù)都是局部變量,變量的地址空間在線程??臻g內(nèi),這種情況,變量歸屬單個(gè) 線程,其他線程無法獲得這種變量。但有時(shí)候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互。
? ? ? ? 下面的搶票例子中,多線程之間訪問同一個(gè)全局變量會(huì)出現(xiàn)票數(shù)多賣的情況:
int tickets=1000;
void *getTickets(void *args)
{(void)args;while(true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}return nullptr;
}
int main()
{pthread_t tid[5];char name[64];for(int i=1;i<=5;i++){//創(chuàng)建多個(gè)線程pthread_create(&tid[i-1],nullptr,getTickets,nullptr);}for(int i=0;i<5;i++){pthread_join(tid[i],nullptr);}return 0;
}
????????我們發(fā)現(xiàn)有些線程會(huì)出現(xiàn)搶到負(fù)數(shù)票的情況。
????????這其中的原因就是多線程對(duì)不加保護(hù)的臨界變量進(jìn)行并發(fā)執(zhí)行的問題。票數(shù)tickets進(jìn)行自減的操作看似只有一行代碼,實(shí)際上對(duì)應(yīng)三條匯編指令,因此tickets的自減操作并非原子操作。CPU對(duì)tickets的操作需要分為三步:第一步,load :將共享變量tickets從內(nèi)存加載到寄存器中;第二步,update : 更新寄存器里面的值,執(zhí)行-1操作;第三步:store :將新值,從寄存器寫回共享變量tickets的內(nèi)存地址。這三步在一個(gè)線程執(zhí)行過程中會(huì)有可能在任意一步進(jìn)行切走,執(zhí)行另外的線程,其他線程又會(huì)訪問該變量。
????????多個(gè)線程經(jīng)過這種不加保護(hù)的操作后,tickets出現(xiàn)混亂,從而導(dǎo)致票數(shù)多賣。
????????要解決以上問題,需要做到三點(diǎn):
(1)代碼必須要有互斥行為:當(dāng)代碼進(jìn)入臨界區(qū)執(zhí)行時(shí),不允許其他線程進(jìn)入該臨界區(qū)。
(2)如果多個(gè)線程同時(shí)要求執(zhí)行臨界區(qū)的代碼,并且臨界區(qū)沒有線程在執(zhí)行,那么只能允許一個(gè)線程進(jìn)入該臨界區(qū)。
(3)如果線程不在臨界區(qū)中執(zhí)行,那么該線程不能阻止其他線程進(jìn)入臨界區(qū)。
????????本質(zhì)上我們需要線程獨(dú)立的訪問臨界數(shù)據(jù)區(qū),需要一把鎖將該區(qū)域進(jìn)行鎖住,Linux上提供的這把鎖叫互斥量。
4.2互斥量的函數(shù)接口
(1)創(chuàng)建互斥量
靜態(tài)分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
????????鎖是全局的變量時(shí),使用宏P(guān)THREAD_MUTEX_INITIALIZER進(jìn)行初始化。
動(dòng)態(tài)分配:當(dāng)鎖是局部變量時(shí),需要調(diào)用初始化函數(shù)pthread_mutex_init進(jìn)行初始化。
頭文件:#include<pthread.h>
函數(shù)體:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
參數(shù):mutex:要初始化的互斥量
? ? ? ? ? ?attr:NULL
返回值:成功返回0,失敗返回錯(cuò)誤號(hào)
(2)銷毀互斥量
頭文件:#include<pthread.h>
函數(shù)體:int pthread_mutex_destroy(pthread_mutex_t *mutex);
參數(shù):mutex:要銷毀的互斥量
返回值:成功返回0,失敗返回錯(cuò)誤號(hào)
(3)加鎖和解鎖
頭文件:#include<pthread.h>
函數(shù)體:int pthread_mutex_lock(pthread_mutex_t *mutex);
??????????????int pthread_mutex_unlock(pthread_mutex_t *mutex);
參數(shù):mutex:要加鎖或者解鎖的互斥量
返回值:成功返回0,失敗返回錯(cuò)誤號(hào)
????????此時(shí)將搶票邏輯進(jìn)行加鎖控制,此時(shí)就不會(huì)出現(xiàn)數(shù)據(jù)紊亂的問題了。
void *getTickets(void *mtx)
{while(true){pthread_mutex_lock((pthread_mutex_t*)mtx);if(tickets > 0){usleep(rand()%1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;pthread_mutex_unlock((pthread_mutex_t*)mtx);}else{pthread_mutex_unlock((pthread_mutex_t*)mtx);break;}}usleep(rand()%200000);return nullptr;
}
????????由此我們可以得出,在進(jìn)行加鎖之后,線程之間執(zhí)行臨界區(qū)的代碼時(shí)是串行的,那么加了鎖之后線程在臨界區(qū)還是會(huì)進(jìn)行切換的,但是此時(shí)的切換是帶著鎖進(jìn)行切換的,其他線程想要訪問臨界區(qū)的資源還是需要先申請(qǐng)鎖,鎖無法申請(qǐng)成功,所以此時(shí)還是無法訪問臨界資源,從而確保了臨界區(qū)的資源的安全性。注意:加鎖的粒度需要越細(xì)越好。
4.3深入理解申請(qǐng)和釋放鎖
????????現(xiàn)在我們明白了,臨界區(qū)的代碼添加鎖后就能保證多個(gè)線程訪問共享數(shù)據(jù)的唯一性,也就是這把鎖是每個(gè)線程都能看到的資源。那么這把鎖不也是一種共享資源嗎?那么誰(shuí)來保證鎖的安全呢?換句話說,申請(qǐng)和釋放鎖也必須是原子性的。這就陷入了循環(huán)死穴。
? ? ? ? 其實(shí),鎖的原子性是由鎖本身來保證的。
????????在CPU執(zhí)行計(jì)算時(shí),如果只有一條匯編語(yǔ)句,那么就認(rèn)為該匯編語(yǔ)句的執(zhí)行是原子的。為了實(shí)現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了swap或exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺(tái),訪問內(nèi)存的總線周期也有先后,一 個(gè)處理器上的交換指令執(zhí)行時(shí)另一個(gè)處理器的交換指令只能等待總線周期。
????????而lock和unlock的偽代碼如下所示,我們進(jìn)行分析:
????????首先我們要知道,多個(gè)線程共享CPU寄存器的空間,但是寄存器里面的內(nèi)容是每個(gè)線程的上下文數(shù)據(jù),是私有的,在被切換時(shí)會(huì)帶走。
????????整個(gè)過程中,mtx的1全程只有一個(gè),線程A,B都是通過交換得到的,線程A交換走,線程B就不會(huì)得到,從而保證了原子性 。
4.4可重入函數(shù)和線程安全
線程安全:
????????多個(gè)線程并發(fā)同一段代碼時(shí),不會(huì)出現(xiàn)不同的結(jié)果。常見對(duì)全局變量或者靜態(tài)變量進(jìn)行操作, 并且沒有鎖保護(hù)的情況下,會(huì)出現(xiàn)該問題。
重入:
????????同一個(gè)函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個(gè)流程還沒有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,我們稱之為重入。一個(gè)函數(shù)在重入的情況下,運(yùn)行結(jié)果不會(huì)出現(xiàn)任何不同或者任何問題,則該函數(shù)被稱為可重入函數(shù),否則,是不可重入函數(shù)。
線程不安全的情況:
????????不保護(hù)共享變量的函數(shù);函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù);返回指向靜態(tài)變量指針的函數(shù);調(diào)用線程不安全函數(shù)的函數(shù)。
線程安全的情況:
????????每個(gè)線程對(duì)全局變量或者靜態(tài)變量只有讀取的權(quán)限,而沒有寫入的權(quán)限,一般來說這些線程是安全的;類或者接口對(duì)于線程來說都是原子操作;多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性。
不可重入的情況:
????????調(diào)用了malloc/free函數(shù),因?yàn)閙alloc函數(shù)是用全局鏈表來管理堆的;調(diào)用了標(biāo)準(zhǔn)I/O庫(kù)函數(shù),標(biāo)準(zhǔn)I/O庫(kù)的很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu);可重入函數(shù)體內(nèi)使用了靜態(tài)的數(shù)據(jù)結(jié)構(gòu)。
可重入的情況:
????????不使用全局變量或靜態(tài)變量;不使用用malloc或者new開辟出的空間;不調(diào)用不可重入函數(shù); 不返回靜態(tài)或全局?jǐn)?shù)據(jù),所有數(shù)據(jù)都有函數(shù)的調(diào)用者提供;使用本地?cái)?shù)據(jù),或者通過制作全局?jǐn)?shù)據(jù)的本地拷貝來保護(hù)全局?jǐn)?shù)據(jù)。
可重入與線程安全的聯(lián)系和區(qū)別:
????????函數(shù)是可重入的,那就是線程安全的;函數(shù)是不可重入的,那就不能由多個(gè)線程使用,有可能引發(fā)線程安全問題;如果一個(gè)函數(shù)中有全局變量,那么這個(gè)函數(shù)既不是線程安全也不是可重入的。
????????可重入函數(shù)是線程安全函數(shù)的一種。線程安全不一定是可重入的,而可重入函數(shù)則一定是線程安全的。 如果將對(duì)臨界資源的訪問加上鎖,則這個(gè)函數(shù)是線程安全的,但如果這個(gè)重入函數(shù)若鎖還未釋放則會(huì)產(chǎn)生死鎖,因此是不可重入的。
4.5死鎖概念
????????死鎖是指在一組進(jìn)程中的各個(gè)進(jìn)程均占有不會(huì)釋放的資源,但因互相申請(qǐng)被其他進(jìn)程所站用不會(huì)釋放的資 源而處于的一種永久等待狀態(tài)。
????????多個(gè)鎖的申請(qǐng)和釋放會(huì)造成死鎖,例如線程A申請(qǐng)鎖1成功后,去申請(qǐng)鎖2,發(fā)現(xiàn)鎖2被線程B申請(qǐng)了,線程A只能掛起等待,而線程B在執(zhí)行過程中,又去申請(qǐng)鎖1,發(fā)現(xiàn)線程A申請(qǐng)了,只能掛起等待,此時(shí)兩個(gè)線程陷入死鎖,互相等待。
? ? ? ? 一把鎖也有可能造成死鎖,例如線程A申請(qǐng)鎖之后沒有釋放,再去申請(qǐng)時(shí)就會(huì)造成死鎖。
死鎖四個(gè)必要條件:
(1)互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用
(2)請(qǐng)求與保持條件:一個(gè)執(zhí)行流因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放
(3)不剝奪條件:一個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
(4)循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
避免死鎖:
(1)破壞死鎖的四個(gè)必要條件
(2)加鎖順序一致
(3)避免鎖未釋放的場(chǎng)景
(4)資源一次性分配
避免死鎖算法:死鎖檢測(cè)算法,銀行家算法。
5.線程同步
5.1為什么需要線程同步
????????通過互斥鎖的使用,我們能夠確保臨界資源的安全。但是線程在使用互斥鎖時(shí)還會(huì)帶了一個(gè)問題,如果一個(gè)線程頻繁的申請(qǐng)互斥鎖,那么其他的線程就得等待,線程的等待沒有秩序,誰(shuí)搶到就是誰(shuí)的。線程在申請(qǐng)臨界資源之前一定要先對(duì)臨界資源的存在做出檢測(cè),而對(duì)臨界資源檢測(cè)的本質(zhì)也是訪問臨界資源,這就意味著對(duì)臨界資源的檢測(cè)也一定需要在加鎖和解鎖之間。那么那些等待臨界資源的線程就必然需要頻繁的申請(qǐng)和釋放鎖,帶來極大的資源浪費(fèi)。
? ? ? ? 線程同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題。存在的目的就是為了解決這些線程訪問臨界資源合理性的問題。
? ? ? ? 競(jìng)態(tài)條件:因?yàn)闀r(shí)序問題,而導(dǎo)致程序異常,我們稱之為競(jìng)態(tài)條件。
????????如果我們能夠讓線程在資源不就緒的時(shí)候進(jìn)行等待,而不是頻繁的進(jìn)行臨界資源的申請(qǐng),等到臨界資源滿足條件就緒了,就通知對(duì)應(yīng)的線程,讓其來進(jìn)行資源的申請(qǐng)和訪問。這就需要條件變量。
5.2條件變量函數(shù)
(1)條件變量的初始化函數(shù)
????????當(dāng)定義全局的條件變量時(shí),可以使用PTHREAD_COND_INITIALIZER進(jìn)行初始化。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
????????當(dāng)條件變量為局部變量時(shí),需要調(diào)用初始化函數(shù)進(jìn)行初始化。
頭文件:#include<pthread.h>
函數(shù)體:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
參數(shù):
????????cond:要初始化的條件變量
????????attr:NULL
返回值:? 成功返回0,失敗返回錯(cuò)誤碼
(2)銷毀函數(shù)
頭文件:#include<pthread.h>
函數(shù)體:int pthread_cond_destroy(pthread_cond_t *cond);
參數(shù):cond:要銷毀的條件變量
返回值:? 成功返回0,失敗返回錯(cuò)誤碼
(3)等待條件函數(shù)
頭文件:#include<pthread.h>
函數(shù)體:int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
參數(shù):cond:要在這個(gè)條件變量上等待
? ? ? ? ? mutex:互斥量
返回值:? 成功返回0,失敗返回錯(cuò)誤碼
????????為什么 pthread_cond_wait 需要互斥量?
????????條件等待是線程間同步的一種手段,如果只有一個(gè)線程,條件不滿足,一直等下去都不會(huì)滿足,所以必須要有一個(gè)線程通過某些操作,改變共享變量,使原先不滿足的條件變得滿足,并且友好的通知等待在條件變量上的線程。 條件不會(huì)無緣無故的突然變得滿足了,必然會(huì)牽扯到共享數(shù)據(jù)的變化。所以一定要用互斥鎖來保護(hù)。沒有互斥鎖就無法安全的獲取和修改共享數(shù)據(jù)。
(4)喚醒等待
頭文件:#include<pthread.h>
函數(shù)體:int pthread_cond_broadcast(pthread_cond_t *cond);//一次喚醒一批線程
? ? ? ? ? ? ? int?pthread_cond_signal(pthread_cond_t *cond);//一次喚醒一個(gè)線程
參數(shù):cond:要喚醒的條件變量
返回值:? 成功返回0,失敗返回錯(cuò)誤碼
代碼練習(xí):生產(chǎn)者消費(fèi)者模型。
6.POSIX信號(hào)量
????????前面的章節(jié)中我們提到過信號(hào)量,并且將其視為一個(gè)“計(jì)數(shù)器”?,F(xiàn)在我們深入了解一下信號(hào)量。這里我們所說的信號(hào)量是POSIX信號(hào)量,它可以支持線程同步。我們都知道在訪問共享資源的時(shí)候,對(duì)于臨界區(qū)的資源必須要確保只有一個(gè)執(zhí)行流來進(jìn)行訪問,因?yàn)橹挥羞@樣才是安全的。但是有時(shí)臨界區(qū)具備多種臨界資源,每個(gè)線程想要獲取的或許是不同的,如果都要加鎖解鎖來訪問,效率必然降低。因此,我們可以在訪問前進(jìn)行申請(qǐng),如果資源具備,那線程就直接拿走,其他線程同時(shí)也可以申請(qǐng),就如同我們買點(diǎn)影票一樣,只有里面的資源不再具備,此時(shí)線程申請(qǐng)就會(huì)失敗,哪個(gè)線程都一樣,都必須等待。只有線程訪問的資源相同時(shí)才進(jìn)行加速解鎖操作。
? ? ? ? 所以在對(duì)資源進(jìn)行使用時(shí)我們先進(jìn)行申請(qǐng),就是信號(hào)量的P操作,使用完畢后對(duì)其進(jìn)行釋放,就是信號(hào)量的V操作。具體的函數(shù)如下:
6.1信號(hào)量操作函數(shù)
(1)初始化信號(hào)量
頭文件:#include?<semaphore.h>?
函數(shù)體:int sem_init(sem_t *sem, int pshared, unsigned int value);?
參數(shù):
???????????????pshared:0表示線程間共享,非零表示進(jìn)程間共享 ?
???????????????value:信號(hào)量初始值?
返回值:? 成功返回0,失敗返回-1,并且設(shè)置錯(cuò)誤碼
(2)銷毀信號(hào)量
頭文件:#include?<semaphore.h>?
函數(shù)體:int sem_destroy(sem_t *sem);?
參數(shù):要銷毀的信號(hào)量
返回值:? 成功返回0,失敗返回-1,并且設(shè)置錯(cuò)誤碼
(3)等待信號(hào)量
頭文件:#include?<semaphore.h>?
函數(shù)體:int sem_wait(sem_t *sem);??
參數(shù):等待的信號(hào)量
返回值:? 成功返回0,失敗返回-1,并且設(shè)置錯(cuò)誤碼
(4)發(fā)布信號(hào)量?
頭文件:#include?<semaphore.h>?
函數(shù)體:int sem_post(sem_t *sem);
參數(shù):要發(fā)布的信號(hào)量
返回值:? 成功返回0,失敗返回-1,并且設(shè)置錯(cuò)誤碼
6.2環(huán)形隊(duì)列的生產(chǎn)消費(fèi)模型
????????環(huán)形隊(duì)列中一個(gè)線程進(jìn)行數(shù)據(jù)的生產(chǎn),一個(gè)線程進(jìn)行數(shù)據(jù)的消費(fèi),如果此時(shí)兩個(gè)線程訪問的并非同一個(gè)數(shù)據(jù),那么就不會(huì)出現(xiàn)線程安全問題,只有在同時(shí)訪問同一個(gè)數(shù)據(jù)的時(shí)候才會(huì)出現(xiàn)數(shù)據(jù)二義性的問題。
? ? ? ? 例如在下面的情況,線程A在生產(chǎn)了數(shù)據(jù)-4,線程B正在拿走數(shù)據(jù)6,此時(shí)兩個(gè)線程并不干擾,不需要加鎖來進(jìn)行保護(hù)。當(dāng)環(huán)形隊(duì)列中數(shù)據(jù)為空時(shí),線程B想要消費(fèi)就必須等待線程A進(jìn)行生產(chǎn),環(huán)形隊(duì)列數(shù)據(jù)滿了時(shí),線程A想生產(chǎn)就必須讓線程B進(jìn)行消費(fèi)之后才能生產(chǎn)。
? ? ? ? ?所以,線程A扮演的生產(chǎn)者需要的是空間資源,具備空間資源才能生產(chǎn)數(shù)據(jù)。線程B扮演的消費(fèi)者需要數(shù)據(jù)資源,消費(fèi)了數(shù)據(jù)資源才能具備空間。所以此時(shí)我們就可以引入信號(hào)量進(jìn)行生產(chǎn),當(dāng)生產(chǎn)者進(jìn)行生產(chǎn)時(shí),先去申請(qǐng)空間資源,申請(qǐng)成功則空間資源信號(hào)量自減,并進(jìn)行數(shù)據(jù)生產(chǎn),生產(chǎn)完數(shù)據(jù)后,將數(shù)據(jù)資源的信號(hào)量進(jìn)行自增,申請(qǐng)失敗則需要等待空間資源就緒。同理,消費(fèi)者消費(fèi)時(shí),也要申請(qǐng)數(shù)據(jù)資源,成功則數(shù)據(jù)資源自減,失敗則說明沒有數(shù)據(jù)可以消費(fèi),需要等待生產(chǎn)者進(jìn)行生產(chǎn)。消費(fèi)成功后,空間資源就會(huì)留出,空間資源自增。
具體代碼連接如下:基于信號(hào)量的環(huán)形隊(duì)列