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