
在上節(jié)內(nèi)容中我們學(xué)習(xí)了線程篇五這節(jié)內(nèi)容我們詳細(xì)講解線程同步.線程同步這是上節(jié)內(nèi)容我們描寫的是模擬搶票系統(tǒng)先看這段經(jīng)典的多線程搶票邏輯雖然我們給它加上了互斥鎖來保護(hù)數(shù)據(jù)但這就像給自習(xí)室門口安了一把只能單人進(jìn)出的鎖依然治不了“搶票狂魔”的問題——那個(gè)離鎖最近、手速最快的線程總是能搶在別人前面一遍遍地把票掃光導(dǎo)致其他線程連湯都喝不上只能餓著肚子空轉(zhuǎn)。這種現(xiàn)象在計(jì)算機(jī)里叫“饑餓問題”這個(gè)在上節(jié)內(nèi)容中給大家講過它本身沒有語法錯(cuò)誤但極其不合理就好比鑰匙剛從墻上的掛鉤摘下還沒來得及給別人用同一個(gè)學(xué)生又順手拿了進(jìn)去反復(fù)循環(huán)只圖自己方便。實(shí)際上真正合理的做法必須加上一條“排隊(duì)”規(guī)則任何人用完鑰匙掛回墻上后不能立刻再申請必須老老實(shí)實(shí)去隊(duì)伍后面排隊(duì)。把這個(gè)“按順序辦事”的規(guī)則落實(shí)到代碼里來保證所有人都能公平地獲取資源就是我們要引入的核心概念——線程同步。排隊(duì)的本質(zhì)就是在互斥保證安全的基礎(chǔ)上賦予線程一個(gè)公平的申請與釋放順序這就是線程同步。它解決的不是數(shù)據(jù)對不對而是分配合理不合理的問題。正因如此互斥和同步總是成對出現(xiàn)光有互斥始終無法保證臨界資源得到有序的合理訪問。要說線程同步我們就要引入條件變量1.條件變量概念條件變量本質(zhì)上是原生線程庫提供的一個(gè)同步機(jī)制。它不是一個(gè)用來存數(shù)據(jù)的普通變量而是一個(gè)用來描述臨界資源當(dāng)前狀態(tài)的信號器。它的作用類似于消息通知讓一個(gè)線程可以掛起等待直到另一個(gè)線程改變了共享資源的狀態(tài)并通過條件變量發(fā)出資源就緒的信號來喚醒它。我們在寫搶票程序時(shí)很容易陷入一個(gè)誤區(qū)如果發(fā)現(xiàn)票賣完了線程還是會拼命去申請鎖、檢查、再釋放鎖循環(huán)往復(fù)這個(gè)是錯(cuò)誤的而是不應(yīng)該再申請鎖了而應(yīng)該等有票了再申請。這就像中午你想去飲水機(jī)接水走過去一看桶空了你走開了過了兩秒你又跑過去看又空了再走開……反復(fù)折騰。邏輯上你確實(shí)沒做錯(cuò)但這種“輪詢”不僅浪費(fèi)了你的腿腳CPU計(jì)算資源還讓本來想接水的人總要被你擋一下鎖的爭搶加劇。實(shí)際上更合理的做法是既然是沒水了你就應(yīng)該在飲水機(jī)旁邊貼張紙條告訴送水工水來了叫我一聲。然后你在旁邊安心休息等送水工換好新桶敲敲你的肩膀喚醒你你再去接水。這里的“紙條”或“喚醒信號”就是用來描述臨界資源狀態(tài)水有沒有的關(guān)鍵也就是我們要引入的條件變量。之所以之前我們一直在盲目地輪詢申請鎖根本原因就是線程之間缺乏溝通大家完全不清楚臨界資源當(dāng)前的狀況只能靠撞大運(yùn)般地去查。在張圖左邊負(fù)責(zé)“放蘋果”右邊負(fù)責(zé)“拿蘋果”中間有個(gè)盤子臨界資源。首先我們要明白放和拿的動(dòng)作是連續(xù)的如果不加控制就會出現(xiàn)二義性——萬一我這邊還在放你那邊就來拿了誰說得準(zhǔn)這蘋果到底算不算已經(jīng)放好了所以鎖的作用就是強(qiáng)制規(guī)定不管你是放還是拿必須先拿鑰匙加鎖把整個(gè)過程變成不可分割的“原子操作”保證盤子里的狀態(tài)是明確的。但光有鎖還不夠。右邊有個(gè)典型的輪詢陷阱放蘋果的人手腳麻利拿蘋果的人磨磨蹭蹭。如果兩人都是瞎子互相不知道對方動(dòng)作放蘋果的人就會反復(fù)加鎖、一看蘋果還在、解鎖接著馬上又加鎖……他反反復(fù)復(fù)地瞎忙活雖然代碼沒錯(cuò)但極其浪費(fèi)CPU而行動(dòng)慢的拿蘋果的人連搶到鎖的機(jī)會都沒有陷入了“饑餓”。這時(shí)圖里紅框標(biāo)出的“鈴鐺”條件變量和旁邊的“隊(duì)列”就派上用場了。合理的做法是放蘋果的人加好鎖、放下蘋果解鎖后立刻敲一下鈴鐺然后去旁邊掛起睡覺。這個(gè)鈴鐺的作用就是描述當(dāng)前盤子臨界資源有沒有蘋果的狀態(tài)。另一邊拿蘋果的人并不是盲目地去搶鎖而是先在隊(duì)列等待隊(duì)列里好好排隊(duì)等著。一旦聽到鈴鐺響他知道有蘋果了就會從隊(duì)列里被喚醒從容地去加鎖、拿走蘋果并敲鈴鐺通知對方。當(dāng)?個(gè)線程互斥地訪問某個(gè)變量時(shí)它可能發(fā)現(xiàn)在其它線程改變狀態(tài)之前它什么也做不了。例如?個(gè)線程訪問隊(duì)列時(shí)發(fā)現(xiàn)隊(duì)列為空它只能等待只到其它線程將?個(gè)節(jié)點(diǎn)添加到隊(duì)列中。這種情況就需要?到條件變量。2.同步概念與競態(tài)條件同步在保證數(shù)據(jù)安全的前提下讓線程能夠按照某種特定的順序訪問臨界資源從?有效避免饑餓問題叫做同步競態(tài)條件因?yàn)闀r(shí)序問題?導(dǎo)致程序異常我們稱之為競態(tài)條件。在線程場景下這種問題也不難理解詳細(xì)內(nèi)容上一節(jié)我都講過了3.條件變量函數(shù)函數(shù)接口介紹初始化int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);參數(shù)cond要初始化的條件變量attrNULL銷毀int pthread_cond_destroy(pthread_cond_t *cond)等待條件滿足pthread_cond_wait(cond, mutex)使調(diào)用線程在指定條件變量cond上阻塞并掛入其等待隊(duì)列。關(guān)鍵機(jī)制阻塞掛起的瞬間系統(tǒng)會原子性地釋放互斥鎖mutex避免死鎖當(dāng)被喚醒后系統(tǒng)又會原子性地重新獲取該鎖之后線程才得以繼續(xù)執(zhí)行后文生產(chǎn)者消費(fèi)者模型將詳細(xì)拆解。pthread_cond_timedwait(cond, mutex, abstime)接口與wait完全一致僅多出超時(shí)控制參數(shù)abstime絕對時(shí)間。若指定時(shí)間內(nèi)未收到喚醒信號線程將自動(dòng)解除阻塞。主要用于規(guī)避無限等待的風(fēng)險(xiǎn)實(shí)際應(yīng)用場景相對有限。參數(shù)cond要在這個(gè)條件變量上等待mutex互斥量后?詳細(xì)解釋喚醒等待pthread_cond_signal(cond)喚醒在指定條件變量cond的等待隊(duì)列中阻塞的第一個(gè)線程。常用于資源僅能滿足單個(gè)消費(fèi)者的情況。pthread_cond_broadcast(cond)喚醒所有當(dāng)前在該條件變量cond下阻塞等待的線程。多用于資源狀態(tài)發(fā)生重大變化如一次性釋放了多個(gè)資源需要通知所有等待者重新競爭應(yīng)用場景相對較少。4.為什么 pthread_ cond_ wait 需要互斥量?條件等待是線程間同步的?種?段如果只有?個(gè)線程條件不滿??直等下去都不會滿?所以必須要有?個(gè)線程通過某些操作改變共享變量使原先不滿?的條件變得滿?并且友好的通知等待在條件變量上的線程。條件不會?緣?故的突然變得滿?了必然會牽扯到共享數(shù)據(jù)的變化。所以?定要?互斥鎖來保護(hù)。沒有互斥鎖就?法安全的獲取和修改共享數(shù)據(jù)。按照上?的說法我們設(shè)計(jì)出如下的代碼先上鎖發(fā)現(xiàn)條件不滿?解鎖然后等待在條件變量上不就?了如下代碼:// 錯(cuò)誤示范存在致命的信號丟失風(fēng)險(xiǎn) pthread_mutex_lock(mutex); while (condition_is_false) { pthread_mutex_unlock(mutex); // 步驟1主動(dòng)解鎖 //解鎖之后等待之前條件可能已經(jīng)滿足信號已經(jīng)發(fā)出但是該信號可能被錯(cuò)過 pthread_cond_wait(cond); // 步驟2此時(shí)才開始去排隊(duì)等待 pthread_mutex_lock(mutex); // 步驟3醒來后自己搶鎖 } pthread_mutex_unlock(mutex);由于解鎖和等待不是原?操作。調(diào)?解鎖之后 pthread_cond_wait 之前如果已經(jīng)有其他線程獲取到互斥量摒棄條件滿?發(fā)送了信號那么 pthread_cond_wait 將錯(cuò)過這個(gè)信號可能會導(dǎo)致線程永遠(yuǎn)阻塞在這個(gè) pthread_cond_wait 。所以解鎖和等待必須是?個(gè)原?操作。int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t *mutex); 進(jìn)?該函數(shù)后會去看條件量等于0不等于就把互斥量變成1直到cond_ wait返回把條件量改成1把互斥量恢復(fù)成原樣。5.條件變量使用規(guī)范1.等待條件代碼pthread_mutex_lock(mutex); // 必須使用 while 循環(huán)而不是 if來防止假的喚醒 while (條件為假) { pthread_cond_wait(cond, mutex); } // 此時(shí)條件為真可以安全地操作共享資源 修改條件; pthread_mutex_unlock(mutex);在這里為什么while是必須的因?yàn)榧词贡籹ignal喚醒了也可能因?yàn)橄到y(tǒng)調(diào)度或競爭導(dǎo)致共享變量狀態(tài)在被喚醒的瞬間又被其他線程搶走了。使用while可以確保線程醒來后重新檢查一次如果條件又不滿足了就繼續(xù)回去等。給條件發(fā)送信號代碼pthread_mutex_lock(mutex); // 修改共享變量使等待的條件變?yōu)檎?設(shè)置條件為真; // 發(fā)出信號喚醒在 cond 上等待的一個(gè)線程 pthread_cond_signal(cond); pthread_mutex_unlock(mutex);修改條件和發(fā)送信號必須在同一個(gè)互斥鎖的保護(hù)下完成。這樣可以保證通知者寫數(shù)據(jù)時(shí)等待者不會同時(shí)讀數(shù)據(jù)數(shù)據(jù)一致且信號不會被錯(cuò)過。代碼如下#include iostream #include string #include unistd.h #include pthread.h #define TNUM 4 typedef void (*func_t)(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond); volatile bool quit false; // pthread_cond_t cond PTHREAD_COND_INITIALIZER; // pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER; class ThreadData { public: ThreadData(const std::string name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond) :name_(name), func_(func), pmtx_(pmtx), pcond_(pcond) {} public: std::string name_; func_t func_; pthread_mutex_t *pmtx_; pthread_cond_t *pcond_; }; void func1(const std::string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { // wait一定要在加鎖和解鎖之間進(jìn)行wait pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); //默認(rèn)該線程在執(zhí)行時(shí)wait代碼被執(zhí)行當(dāng)前線程會被立即被阻塞 std::cout name running -- 播放 std::endl; pthread_mutex_unlock(pmtx); } } void func2(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); if(!quit) std::cout name running -- 下載 std::endl; pthread_mutex_unlock(pmtx); } } void func3(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); std::cout name running -- 刷新 std::endl; pthread_mutex_unlock(pmtx); } } void func4(const std::string name,pthread_mutex_t *pmtx, pthread_cond_t *pcond) { while(!quit) { pthread_mutex_lock(pmtx); pthread_cond_wait(pcond, pmtx); std::cout name running -- 掃碼用戶信息 std::endl; pthread_mutex_unlock(pmtx); } } void *Entry(void *args) { ThreadData *td (ThreadData*)args; // td在每一個(gè)線程自己私有的??臻g中保存 td-func_(td-name_, td-pmtx_, td-pcond_); // 它是一個(gè)函數(shù)調(diào)用完成就要返回 delete td; return nullptr; } int main() { pthread_mutex_t mtx; pthread_cond_t cond; pthread_mutex_init(mtx, nullptr); pthread_cond_init(cond, nullptr); pthread_t tids[TNUM]; func_t funcs[TNUM] {func1, func2, func3, func4}; for (int i 0; i TNUM; i) { std::string name Thread ; name std::to_string(i1); ThreadData *td new ThreadData(name, funcs[i], mtx, cond); pthread_create(tids i, nullptr, Entry, (void*)td); } sleep(5); // ctrl new thread int cnt 10; while(cnt) { std::cout resume thread run code .... cnt-- std::endl; pthread_cond_signal(cond); // pthread_cond_broadcast(cond); sleep(1); } std::cout ctrl done std::endl; quit true; pthread_cond_broadcast(cond); for(int i 0; i TNUM; i) { pthread_join(tids[i], nullptr); std::cout thread: tids[i] quit std::endl; } pthread_mutex_destroy(mtx); pthread_cond_destroy(cond); return 0; }這段代碼主要是利用互斥鎖和條件變量實(shí)現(xiàn)線程的休眠與喚醒控制。程序啟動(dòng)之后主線程先創(chuàng)建四個(gè)子線程各個(gè)子線程運(yùn)行后就調(diào)用 pthread_cond_wait 進(jìn)入阻塞等待狀態(tài)此時(shí)線程就掛在條件變量的等待隊(duì)列上。主線程等待幾秒之后循環(huán)調(diào)用 pthread_cond_signal 每次喚醒一個(gè)子線程被喚醒的線程拿到鎖就執(zhí)行對應(yīng)的業(yè)務(wù)打印執(zhí)行完解鎖后又會再次進(jìn)入等待。等喚醒循環(huán)結(jié)束把全局退出標(biāo)記 quit 置為 true再通過 pthread_cond_broadcast 喚醒所有線程讓各個(gè)子線程跳出循環(huán)正常結(jié)束最后主線程 join 回收線程資源銷毀鎖與條件變量完成整個(gè)程序的收尾。6. 條件變量的封裝基于上?的基本認(rèn)識我們已經(jīng)知道條件變量如何使?雖然細(xì)節(jié)需要后?再來進(jìn)?解釋但這?可以做?下基本的封裝#pragma once #include iostream #include string #include pthread.h #include Lock.hpp namespace CondModule { using namespace LockModule; class Cond { public: Cond() { int n pthread_cond_init(_cond, nullptr); (void)n; // 實(shí)際工程中此處應(yīng)做 n ! 0 的異常判斷或日志記錄 } void Wait(Mutex *mutex) { // 傳入底層互斥鎖的指針 int n pthread_cond_wait(_cond, mutex-GetMutexOriginal()); (void)n; } void Notify() { int n pthread_cond_signal(_cond); (void)n; } void NotifyAll() { int n pthread_cond_broadcast(_cond); (void)n; } ~Cond() { int n pthread_cond_destroy(_cond); (void)n; // 實(shí)際工程中此處應(yīng)做 n ! 0 的異常判斷或日志記錄 } private: pthread_cond_t _cond; }; }原來的 C 語言做法你必須在主函數(shù)里聲明pthread_cond_t cond;然后調(diào)用pthread_cond_init()初始化最后程序結(jié)束前還得記得調(diào)用pthread_cond_destroy()銷毀。一旦忘了銷毀就會造成內(nèi)存泄露。這個(gè)封裝的思路把底層的 C 變量pthread_cond_t _cond包進(jìn) C 類里。構(gòu)造函數(shù)里自動(dòng)完成初始化析構(gòu)函數(shù)里自動(dòng)完成銷毀。這就變成了只要這個(gè)類對象創(chuàng)建了條件變量就活了對象出了作用域被銷毀了條件變量也就自動(dòng)清理了。同時(shí)你不需要再手動(dòng)去操心初始化和銷毀代碼放在哪了。