你好,我是飛宇。
今天分享一些現代C++并發編程基礎的知識。
分享一個我自己維護的Linux C/C++學習倉庫以及計算機免費PDF電子書倉庫 ,點此查看詳情
并行基礎 std::thread ?用于創建一個執行的線程實例,所以它是一切并發編程的基礎,使用時需要包含? ?頭文件, 它提供了很多基本的線程操作,例如?get_id()? 來獲取所創建線程的線程 ID,使用?join()? 來加入一個線程等等,例如:
#include #include int main () { std ::thread t ([](){ std ::cout << "hello world." << std ::endl ; }); t.join(); return 0 ; }
互斥量與臨界區 我們在操作系統、亦或是數據庫的相關知識中已經了解過了有關并發技術的基本知識,mutex 就是其中的核心之一。C++11 引入了?mutex?相關的類 ,其所有相關的函數都放在? ?頭文件中。
std::mutex? 是 C++11 中最基本的?mutex?類,通過實例化?std::mutex? 可以創建互斥量, 而通過其成員函數 lock()?可以進行上鎖,unlock()?可以進行解鎖。
但是在實際編寫代碼的過程中,最好不去直接調用成員函數, 因為調用成員函數就需要在每個臨界區的出口處調用 unlock(),當然,還包括異常。
這時候 C++11 還為互斥量提供了一個 RAII 語法的模板類 std::lock_guard。RAII 在不失代碼簡潔性的同時,很好的保證了代碼的異常安全性。
在 RAII 用法下,對于臨界區的互斥量的創建只需要在作用域的開始部分,例如:
#include #include int v = 1 ;void critical_section (int change_v) { static std ::mutex mtx; std ::lock_guard<std ::mutex> lock (mtx); // 執行競爭操作 v = change_v; // 離開此作用域后 mtx 會被釋放 } int main () { std::thread t1(critical_section, 2), t2(critical_section, 3); t1.join(); t2.join(); std ::cout << v << std ::endl ; return 0 ; }
由于 C++ 保證了所有棧對象在生命周期結束時會被銷毀,所以這樣的代碼也是異常安全的。無論 critical_section()?正常返回、還是在中途拋出異常,都會引發堆棧回退,也就自動調用了 unlock()。
而?std::unique_lock?則相對于?std::lock_guard?出現的,std::unique_lock?更加靈活,?std::unique_lock?的對象會以獨占所有權 (沒有其他的 unique_lock 對象同時擁有某個 mutex 對象的所有權) 的方式管理 mutex 對象上的上鎖和解鎖的操作。
所以在并發編程中,推薦使用 std::unique_lock。
std::lock_guard?不能顯式的調用?lock?和?unlock, 而?std::unique_lock?可以在聲明后的任意位置調用, 可以縮小鎖的作用范圍,提供更高的并發度 。
如果你用到了條件變量 std::condition_variable::wait 則必須使用 std::unique_lock 作為參數。
例如:
#include #include int v = 1 ;void critical_section (int change_v) { static std ::mutex mtx; std ::unique_lock<std ::mutex> lock (mtx); // 執行競爭操作 v = change_v; std ::cout << v << std ::endl ; // 將鎖進行釋放 lock.unlock(); // 在此期間,任何人都可以搶奪 v 的持有權 // 開始另一組競爭操作,再次加鎖 lock.lock(); v += 1 ; std ::cout << v << std ::endl ; } int main () { std::thread t1(critical_section, 2), t2(critical_section, 3); t1.join(); t2.join(); return 0 ; }
期物 期物(Future)表現為?std::future,它提供了一個訪問異步操作結果的途徑 ,這句話很不好理解。為了理解這個特性,我們需要先理解一下在 C++11 之前的多線程行為。
試想,如果我們的主線程 A 希望新開辟一個線程 B 去執行某個我們預期的任務,并返回我一個結果。
而這時候,線程 A 可能正在忙其他的事情,無暇顧及 B 的結果, 所以我們會很自然的希望能夠在某個特定的時間獲得線程 B 的結果 。
在 C++11 的 std::future 被引入之前,通常的做法是:創建一個線程 A,在線程 A 里啟動任務 B,當準備完畢后發送一個事件,并將結果保存在全局變量中。
而主函數線程 A 里正在做其他的事情,當需要結果的時候,調用一個線程等待函數來獲得執行的結果。
而 C++11 提供的?std::future?簡化了這個流程,可以用來獲取異步任務的結果 。自然地,我們很容易能夠想象到把它作為一種簡單的線程同步手段,即屏障(barrier)。
為了看一個例子,我們這里額外使用 std::packaged_task,它可以用來封裝任何可以調用的目標,從而用于實現異步的調用。舉例來說:
#include #include #include int main () { // 將一個返回值為7的 lambda 表達式封裝到 task 中 // std::packaged_task 的模板參數為要封裝函數的類型 std ::packaged_task<int ()> task ([](){return 7 ;}); // 獲得 task 的期物 std ::future <int > result = task.get_future(); // 在一個線程中執行 task std ::thread(std ::move(task)).detach(); std ::cout << "waiting..." ; result.wait(); // 在此設置屏障,阻塞到期物的完成 // 輸出執行結果 std ::cout << "done!" << std :: endl << "future result is " << result.get() << std ::endl ; return 0 ; }
在封裝好要調用的目標后,可以使用?get_future()?來獲得一個?std::future?對象 ,以便之后實施線程同步。
條件變量 條件變量?std::condition_variable?是為了解決死鎖而生,當互斥操作不夠用而引入的 。比如,線程可能需要等待某個條件為真才能繼續執行, 而一個忙等待循環中可能會導致所有其他線程都無法進入臨界區使得條件為真時,就會發生死鎖。
所以,condition_variable 實例被創建出現主要就是用于喚醒等待線程從而避免死鎖 。?
std::condition_variable的 notify_one()?用于喚醒一個線程;notify_all()?則是通知所有線程。下面是一個生產者和消費者模型的例子:
#include #include #include #include #include #include int main () { std ::queue <int > produced_nums; std ::mutex mtx; std ::condition_variable cv; bool notified = false ; // 通知信號 // 生產者 auto producer = [&]() { for (int i = 0 ; ; i++) { std ::this_thread::sleep_for(std ::chrono::milliseconds(900 )); std ::unique_lock<std ::mutex> lock (mtx); std ::cout << "producing " << i << std ::endl ; produced_nums.push(i); notified = true ; cv.notify_all(); // 此處也可以使用 notify_one } }; // 消費者 auto consumer = [&]() { while (true ) { std ::unique_lock<std ::mutex> lock(mtx); while (!notified) { // 避免虛假喚醒 cv.wait(lock); } // 短暫取消鎖,使得生產者有機會在消費者消費空前繼續生產 lock.unlock(); std ::this_thread::sleep_for(std ::chrono::milliseconds(1000 )); // 消費者慢于生產者 lock.lock(); while (!produced_nums.empty()) { std ::cout << "consuming " << produced_nums.front() << std ::endl ; produced_nums.pop(); } notified = false ; } }; // 分別在不同的線程中運行 std ::thread p (producer); std ::thread cs[2 ]; for (int i = 0 ; i < 2 ; ++i) { cs[i] = std ::thread(consumer); } p.join(); for (int i = 0 ; i < 2 ; ++i) { cs[i].join(); } return 0 ; }
值得一提的是,在生產者中我們雖然可以使用 notify_one(),但實際上并不建議在此處使用, 因為在多消費者的情況下,我們的消費者實現中簡單放棄了鎖的持有,這使得可能讓其他消費者 爭奪此鎖 ,從而更好的利用多個消費者之間的并發。
話雖如此,但實際上因為 std::mutex 的排他性, 我們根本無法期待多個消費者能真正意義上的并行消費隊列的中生產的內容,我們仍需要粒度更細的手段。
來自:現代 C++ 教程:高速上手 C++ 11/14/17/20
鏈接:https://changkun.de/modern-cpp/zh-cn/07-thread/
我的私人微信
你好,我是飛宇,本碩均于某中流985 CS就讀,先后于百度搜索以及字節某電商部門擔任Linux C/C++后端研發工程師。
同時,我也是知乎博主@韓飛宇,日常分享C/C++、計算機學習經驗、工作體會,歡迎點擊閱讀原文 圍觀我分享的C/C++學習知識
??點擊 閱讀原文 查看你錯過的C/C++知識