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