上海網(wǎng)站設(shè)計聯(lián)系方式在線視頻觀看免費視頻22
《c++并發(fā)編程實戰(zhàn)》 筆記
- 1、你好,C++的并發(fā)世界
- 為什么要使用并發(fā)
- 第2章 線程管理
- 2.1.1 啟動線程
- 2.2 向線程函數(shù)傳遞參數(shù)
- 2.5 識別線程
- 第3章 線程間共享數(shù)據(jù)
- 3.2.1 C++中使用互斥量
- 避免死鎖的進階指導(dǎo)
- 保護共享數(shù)據(jù)的替代設(shè)施
- 第4章 同步并發(fā)操作
- 4.1 等待一個事件或其他條件
- 4.2 使用期望等待一次性事件
- 第6章 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計
- 4.2 使用期望等待一次性事件
1、你好,C++的并發(fā)世界
為什么要使用并發(fā)
1、并發(fā)分離關(guān)注點的定義
并發(fā)分離關(guān)注點主要是指在并發(fā)編程中,通過將不同的邏輯或任務(wù)分離到不同的線程或進程中執(zhí)行,從而實現(xiàn)關(guān)注點(即程序中需要特別關(guān)注或處理的部分)的分離。這種分離有助于簡化程序結(jié)構(gòu),提高代碼的可讀性和可維護性。
2、使用并發(fā)提高性能,可用的方法
- 第一個也是最明顯的是將單個任務(wù)劃分為多個部分并并行運行每個部分,從而減少總運行時間。
- 第二個:數(shù)據(jù)并行性(Data Parallelism),即同時對多個數(shù)據(jù)集或數(shù)據(jù)塊執(zhí)行相同的操作或算法。
初始線程始于main(),而新線程始于hello()。
第2章 線程管理
2.1.1 啟動線程
使用C++線程庫啟動線程,可以歸結(jié)為構(gòu)造std::thread對象:
std::thread
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
- Function 是一個可調(diào)用對象(如函數(shù)指針、lambda 表達(dá)式、函數(shù)對象等)的類型。
- Args… 是傳遞給 Function的參數(shù)的類型列表。
std::thread 在 C++ 中扮演著核心角色,特別是在多線程編程領(lǐng)域。它的主要作用是提供一種機制來創(chuàng)建和管理線程,使得程序能夠并行地執(zhí)行多個任務(wù)。
- 創(chuàng)建新線程:通過 std::thread的構(gòu)造函數(shù),可以輕松地創(chuàng)建一個新線程來執(zhí)行指定的函數(shù)或可調(diào)用對象。這允許開發(fā)者將耗時的操作或可以并行處理的任務(wù)放到單獨的線程中執(zhí)行,從而提高程序的性能和響應(yīng)性。
- 管理線程生命周期:std::thread 對象與它所代表的線程緊密相關(guān)。通過調(diào)用 join() 或 detach()方法,可以管理線程的生命周期。join() 方法會阻塞當(dāng)前線程,直到由 std::thread 對象表示的線程完成其執(zhí)行。detach()方法則允許線程獨立于 std::thread 對象運行,此時 std::thread對象不再擁有該線程,且無法再與之交互(除了獲取其ID)。
線程可連接(joinable)是C++中std::thread對象的一個狀態(tài),它表示該std::thread對象代表了一個正在運行或已經(jīng)啟動但尚未結(jié)束的線程。當(dāng)一個std::thread對象被創(chuàng)建并成功啟動了一個新線程時,它就進入了可連接狀態(tài)。
線程資源:
如線程棧、線程控制塊等。如果這些資源在std::thread對象被銷毀時仍未被釋放,就會發(fā)生資源泄露。
在C++中,std::thread對象通過join()或detach()方法來管理其代表的線程。如果std::thread對象在銷毀時仍然是可連接的(即線程仍在運行),且既未調(diào)用join()也未調(diào)用detach(),則程序會調(diào)用std::terminate()來終止執(zhí)行,以防止?jié)撛诘馁Y源泄露。
std::terminate會終止整個程序,而不是線程。操作系統(tǒng)會在程序終止時回收所有由該程序分配的資源。為了防止程序在不確定的線程狀態(tài)下繼續(xù)執(zhí)行,因為此時線程可能還在訪問或操作已經(jīng)銷毀的 std::thread 對象所管理的資源。
std::thread對象通常與特定的執(zhí)行線程相關(guān)聯(lián),并且一旦線程執(zhí)行完畢,std::thread對象就不再擁有任何線程(即變?yōu)榭?#xff09;。
線程的狀態(tài)變化:
-
從非joinable到j(luò)oinable:
- 當(dāng)通過調(diào)用std::thread的構(gòu)造函數(shù)并傳入一個可調(diào)用對象(如函數(shù)指針、Lambda表達(dá)式、綁定表達(dá)式等)來創(chuàng)建std::thread對象時,如果構(gòu)造函數(shù)成功,則新創(chuàng)建的線程對象將處于joinable狀態(tài)。這意呀著你可以對該線程調(diào)用join()來等待它完成,或者調(diào)用detach()來分離它。
-
從joinable到非joinable:
- 一旦對joinable的線程調(diào)用了join()或detach(),該線程對象就不再處于joinable狀態(tài)。對已經(jīng)join或detach的線程對象再次調(diào)用join()或detach()將導(dǎo)致std::system_error異常。
- 如果線程的執(zhí)行函數(shù)已經(jīng)返回(即線程已結(jié)束),并且你尚未對該線程調(diào)用join()或detach(),則嘗試對該線程對象調(diào)用join(),detach仍然有效,但調(diào)用后將使線程對象變?yōu)榉莏oinable。
- 如果線程對象被銷毀時仍然處于joinable狀態(tài)(即沒有調(diào)用join()或detach()),則程序?qū)⒄{(diào)用std::terminate()來終止執(zhí)行,以避免資源泄露。因此,重要的是要確保在銷毀std::thread對象之前,要么調(diào)用join()要么調(diào)用detach()。
2.2 向線程函數(shù)傳遞參數(shù)
參數(shù)要拷貝到線程獨立內(nèi)存中,即使參數(shù)是引用的形式,也可以在新線程中進行訪問。
一定要使用引用:
std::ref 用于創(chuàng)建一個對給定對象的引用封裝器(reference wrapper),這個封裝器可以被存儲在容器中,或者作為函數(shù)參數(shù)傳遞給需要復(fù)制語義但實際上需要引用語義的地方。簡而言之,std::ref 允許你以引用的方式傳遞對象,即使是在需要拷貝的上下文中。
std::bind 是 C++ 標(biāo)準(zhǔn)庫中的一個功能強大的工具,它定義在頭文件 中。std::bind 用于將可調(diào)用對象(如函數(shù)、函數(shù)對象、lambda 表達(dá)式、成員函數(shù)指針等)與其參數(shù)綁定在一起,生成一個新的可調(diào)用對象。這個新的可調(diào)用對象在調(diào)用時,會調(diào)用原始的可調(diào)用對象,并傳遞給它預(yù)先綁定的參數(shù)(如果有的話),以及調(diào)用新可調(diào)用對象時提供的任何額外參數(shù)。
2.5 識別線程
2種方法
- 第一種,可以通過調(diào)用std::thread對象的成員函數(shù)get_id()來直接獲取。如果std::thread對象沒有與任何執(zhí)行線程相關(guān)聯(lián),get_id()將返回std::thread::type默認(rèn)構(gòu)造值,這個值表示“沒有線程”。
- 第二種,當(dāng)前線程中調(diào)用std::this_thread::get_id()(這個函數(shù)定義在頭文件中)也可以獲得線程標(biāo)識。
第3章 線程間共享數(shù)據(jù)
3.2.1 C++中使用互斥量
C++中通過實例化std::mutex創(chuàng)建互斥量,通過調(diào)用成員函數(shù)lock()進行上鎖,unlock()進行解鎖。實踐中更推薦使用RAII語法的模板類std::lock_guard。
加了鎖之后,還需要注意:被保護量不會通過指針或引用方式傳遞到外部去。
避免死鎖的進階指導(dǎo)
死鎖定義:它指的是兩個或多個線程在執(zhí)行過程中,因爭奪資源而造成的一種僵局(互相等待)。
避免死鎖的方法:
- 避免使用多個鎖
- 保持鎖的順序一致
- 避免嵌套鎖
- 使用超時機制
- 使用標(biāo)準(zhǔn)庫中的工具
C++標(biāo)準(zhǔn)庫提供了一些工具來幫助避免死鎖,如std::lock和std::scoped_lock。
std::lock可以一次性為多個互斥量上鎖,并且內(nèi)部使用死鎖避免算法。
std::lock_guard RAII(Resource Acquisition Is Initialization)原則,這意味著資源的獲取(在這里是互斥量的加鎖)是在對象的構(gòu)造函數(shù)中完成的,而資源的釋放(在這里是互斥量的解鎖)則是在對象的析構(gòu)函數(shù)中完成的。
std::scoped_lock是一種RAII(Resource Acquisition Is Initialization)風(fēng)格的鎖管理器,它可以在構(gòu)造時自動上鎖,并在析構(gòu)時自動解鎖,從而簡化了鎖的管理。對多個鎖進行操作,C++ 17引入。
std::unique_lock提供了比 std::lock_guard 更加靈活的互斥量封裝。std::unique_lock 提供了更多的控制,包括延遲加鎖、嘗試加鎖、定時加鎖以及手動解鎖和重新加鎖的能力。 - 持有鎖的時間盡可能少,減少死鎖概率。
保護共享數(shù)據(jù)的替代設(shè)施
保護共享數(shù)據(jù)的初始化過程
雙重檢查鎖存在的問題:
由于C++11之前的內(nèi)存模型并沒有提供足夠的保證來防止指令重排序,因此在多線程環(huán)境中使用雙重檢查鎖模式可能會導(dǎo)致未定義行為,比如訪問未完全初始化的對象。
std::call_once 是 C++11 引入的一個函數(shù),它屬于 頭文件。這個函數(shù)的主要用途是確保某個函數(shù)或可調(diào)用對象只被執(zhí)行一次,即使它被多次調(diào)用。這對于初始化全局變量或執(zhí)行只需要執(zhí)行一次的昂貴操作特別有用。
std::call_once 的使用通常與 std::once_flag 類型的標(biāo)志一起,這個標(biāo)志用來指示函數(shù)是否已經(jīng)被調(diào)用過。如果函數(shù)已經(jīng)被調(diào)用,那么后續(xù)的調(diào)用將不會執(zhí)行任何操作。
在C++11標(biāo)準(zhǔn)中,靜態(tài)局部變量即便在多線程中,也只被初始化一次。
保護很少更新的數(shù)據(jù)結(jié)構(gòu)(讀多寫少)
boost::shared_mutex 是 Boost 庫中的一個同步原語,它允許多個線程以共享模式(shared mode)同時讀取數(shù)據(jù),但寫入數(shù)據(jù)時需要獨占訪問。
使用 boost::shared_lockboost::shared_mutex 來獲取共享鎖。
使用 std::unique_lockboost::shared_mutex 來獲取獨占鎖。
本質(zhì)和讀寫鎖類似。
嵌套鎖
std::recursive_mutex,支持一個線程嘗試鎖多次。適用于獲取鎖的時候用到了其他函數(shù),其他函數(shù)也訪問這個鎖。
更好的替代方法是:考慮是否可以重新設(shè)計函數(shù)邏輯,避免遞歸調(diào)用,從而無需使用遞歸互斥鎖。
第4章 同步并發(fā)操作
線程會等待一個特定事件的發(fā)生,或者等待某一條件達(dá)成(為true)。像這種情況就需要在線程中進行同步,C++標(biāo)準(zhǔn)庫提供了一些工具可用于同步操作,形式上表現(xiàn)為條件變量(condition variables)和期望(futures)。
4.1 等待一個事件或其他條件
條件變量:condition_variable
是利用線程間共享的變量進行同步的一種機制。在多線程程序中,條件變量常用于實現(xiàn)“等待–>喚醒”邏輯,用于維護一個條件(注意區(qū)分條件變量與條件本身),線程可以使用條件變量來等待某個條件為真。
常與鎖結(jié)合。
條件變量的基本原理包括兩個主要動作:
等待:當(dāng)某個條件不滿足時,一個線程會將自己加入等待隊列,并釋放持有的互斥鎖(Mutex),進入睡眠狀態(tài)等待條件成立。wait函數(shù),需要帶條件,可能虛假喚醒。
喚醒:當(dāng)條件滿足時,另一個線程會通知(signal或broadcast)等待在條件變量上的線程,喚醒它們重新檢查條件。被喚醒的線程會重新嘗試獲取互斥鎖,并在獲取鎖后繼續(xù)執(zhí)行。notify_one()與notify_all()。
4.2 使用期望等待一次性事件
std::future
獲得一個 std::future 對象,這個對象將在未來某個時間點持有異步操作的結(jié)果。
關(guān)鍵函數(shù):
- get():這個函數(shù)阻塞當(dāng)前線程,直到異步操作完成,并返回操作的結(jié)果。如果異步操作拋出了異常,get()
函數(shù)將重新拋出該異常。注意,get() 只能被調(diào)用一次,因為一旦結(jié)果被取出,std::future 對象就不再持有任何結(jié)果了。 - wait():這個函數(shù)也會阻塞當(dāng)前線程,但它只是等待異步操作完成,而不返回結(jié)果。如果只是想等待異步操作完成而不關(guān)心結(jié)果,可以使用這個函數(shù)。
- wait_for() 和wait_until():這兩個函數(shù)提供了更靈活的等待機制。它們允許你指定一個時間段或時間點,然后在這個時間段內(nèi)等待異步操作完成。如果操作在這段時間內(nèi)完成了,函數(shù)將返回std::future_status::ready;如果操作沒有完成,函數(shù)將返回 std::future_status::timeout或 std::future_status::deferred(對于 std::async 啟動的異步任務(wù),后者幾乎不會出現(xiàn))。
- valid():這個函數(shù)檢查 std::future 對象是否還持有有效的異步操作結(jié)果。一旦 get()被調(diào)用或異步操作被取消,valid() 將返回 false。
std::async
是 C++11 標(biāo)準(zhǔn)庫中的一個函數(shù)模板,它提供了一種方便的方式來啟動一個異步任務(wù)。當(dāng)你調(diào)用 std::async 時,你可以指定要執(zhí)行的函數(shù)(或可調(diào)用對象)、傳遞給該函數(shù)的參數(shù),以及一個啟動策略(可選)。std::async 返回一個 std::future 對象,這個對象將在未來某個時間點持有異步操作的結(jié)果。
啟動策略:
- std::launch::async:指示 std::async 應(yīng)該異步地執(zhí)行函數(shù),即在新線程或線程池中執(zhí)行。如果系統(tǒng)無法立即啟動新線程,則行為是未定義的(盡管在實際的 C++ 實現(xiàn)中,它通常會阻塞直到能夠啟動新線程)。
- std::launch::deferred:指示 std::async 應(yīng)該延遲執(zhí)行函數(shù),直到調(diào)用返回的 std::future 對象的 wait() 或 get() 方法。此時,函數(shù)將在調(diào)用這些方法的線程中同步執(zhí)行。
如果省略啟動策略,則 std::async 可能會選擇 std::launch::async 或 std::launch::deferred,或者在某些情況下甚至可能使用混合策略。然而,這種混合模式的使用是不確定的,因此最好明確指定你想要的啟動策略。
std::packaged_task<>
是 C++11 標(biāo)準(zhǔn)庫中提供的一個模板類,它封裝了一個可調(diào)用對象(如函數(shù)、lambda 表達(dá)式、綁定表達(dá)式等),使得這個可調(diào)用對象可以異步執(zhí)行。可以通過調(diào)用 std::packaged_task<> 的 operator() 來異步地執(zhí)行封裝的可調(diào)用對象(比std::async更靈活)。
使用std::promises
是 C++11 標(biāo)準(zhǔn)庫中提供的一個類模板,它用于在異步編程中設(shè)置值或異常,以便與 std::future 對象共享這些值或異常。std::promise 和 std::future 一起工作,以支持跨線程的值傳遞和異常傳播。
主要函數(shù):
- get_future(): 返回與 std::promise 對象關(guān)聯(lián)的 std::future 對象。這個函數(shù)只能被調(diào)用一次。
- set_value(T value): 設(shè)置 std::promise 對象的值。這個函數(shù)只能被調(diào)用一次,并且只能在std::promise 對象被銷毀之前調(diào)用。
- set_exception(std::exception_ptr p): 設(shè)置std::promise 對象的異常。這個函數(shù)也只能被調(diào)用一次,并且只能在 std::promise 對象被銷毀之前調(diào)用。
多個線程的等待同一個事件
使用std::shared_future::wait,std::shared_future 與 std::future 類似,但主要區(qū)別在于 std::shared_future 可以被多個線程或?qū)ο蠊蚕?#xff0c;而 std::future 一旦被移動或拷貝后,原始對象將不再持有任何結(jié)果,變成空狀態(tài)。