C++詳細(xì)分析線程間的同步通信
1、多線程編程兩個問題
1.1、線程間的互斥
競態(tài)條件: 多線程執(zhí)行的結(jié)果是一致的,不會隨著CPU對線程不同的調(diào)用順序,而產(chǎn)生不同的運行結(jié)果。
發(fā)生競態(tài)條件的代碼段,稱為臨界區(qū)代碼段(只有一個線程可以進來),保證臨界區(qū)代碼段原子操作,通過線程互斥鎖mutex,也可以使用輕量級的無鎖實現(xiàn)CAS。
C++11的mutex底層實現(xiàn):
使用strace ./a.out跟蹤代碼,使用C++11提供的mutex,Linux底層使用的也是自己的pthread_mutex互斥鎖。
1.2、線程間的同步通信
- 線程間不通信的話,每個線程受CPU的調(diào)度,沒有任何執(zhí)行上的順序可言,線程1和線程2是根據(jù)CPU調(diào)度算法來的,兩個線程都有可能先運行,是不確定的,線程間的運行順序是不確定的;
- 所以多線程程序出問題,難以復(fù)現(xiàn),因為誰也不知道當(dāng)時線程執(zhí)行的先后順序,我們一般可以得到每個線程的線程棧信息來分析是否發(fā)生死鎖的問題之類的。
- 我們要保證線程間的運行順序
通信就是:
- 線程1和線程2一起運行,線程2要做的事情必須先依賴于線程1完成部分的事情,然后告訴線程2這部分東西做好了,線程2就可以繼續(xù)向下執(zhí)行了。
- 或者是線程1接下來要做某些操作,這些操作需要線程2把另外一部分事情做完,然后通知一下線程1它做完了,然后線程1才能做這些操作。
生產(chǎn)者,消費者線程模型
2、生產(chǎn)者-消費者線程模型
注意: C++ STL所有的容器都不是線程安全的,都需要進行封裝。
如果直接使用queue,使用queue的push和pop操作時,會涉及線程安全問題。我們直接在queue的基礎(chǔ)上,直接將其封裝成線程安全的queue。
**注意:**線程函數(shù)代碼和后面的main函數(shù)代碼都是不會變的;
使用lock_gard,不用直接使用互斥鎖的lock和unlock方法,通過棧上的對象構(gòu)造和出作用域析構(gòu),來自動調(diào)用互斥鎖的lock和unlock方法;
封裝的queue代碼,為什么每次出錯都是不一樣的?
消費者消費的比較快,queue為空時,就會出錯;(需要有線程間的通信機制)
我們需要:生產(chǎn)者生產(chǎn)一個物品,通知消費者消費一個;消費完了,消費者再通知生產(chǎn)者繼續(xù)生產(chǎn)物品
條件變量: 可以精確做線程間的同步通信;
信號量也可以做,但是做不到生產(chǎn)一個消費一個,這么精確。
注意: 條件變量cv.wait里面?zhèn)鞯氖莡nique_lock,也只能傳unique_lock,因為lock_gard將拷貝構(gòu)造和賦值重載都delete了(實參到形參是一個拷貝構(gòu)造的過程,傳不進來)
#include <iostream> #include <thread>//多線程的頭文件 #include <mutex>//互斥鎖的頭文件 #include <condition_variable>//條件變量的頭文件 #include <queue>//C++ STL所有的容器都不是線程安全 using namespace std; std::mutex mtx;//定義互斥鎖,做線程間的互斥操作 std::condition_variable cv;//定義條件變量,做線程間的同步通信操作 //生產(chǎn)者生產(chǎn)一個物品,通知消費者消費一個;消費完了,消費者再通知生產(chǎn)者繼續(xù)生產(chǎn)物品 class Queue { public: void put(int val)//生產(chǎn)物品 { unique_lock<std::mutex> lck(mtx);//unique_ptr while (!que.empty()) { //que不為空,生產(chǎn)者應(yīng)該通知消費者去消費,消費者消費完了,生產(chǎn)者再繼續(xù)生產(chǎn) //生產(chǎn)者線程進入#1等待狀態(tài),并且#2把mtx互斥鎖釋放掉 cv.wait(lck);//傳入一個互斥鎖,當(dāng)前線程掛起,處于等待狀態(tài),并且釋放當(dāng)前鎖 lck.lock() lck.unlock } que.push(val); /* notify_one:通知喚醒另外的一個線程的 notify_all:通知喚醒其它所有線程的 通知其它所有的線程,我生產(chǎn)了一個物品,你們趕緊消費吧 其它線程得到該通知,就會從等待狀態(tài) =》 到阻塞狀態(tài) =》 但是要獲取互斥鎖才能繼續(xù)向下執(zhí)行 */ cv.notify_all(); cout << "生產(chǎn)者 生產(chǎn):" << val << "號物品" << endl; } int get()//消費物品 { lock_guard<std::mutex> guard(mtx);//相當(dāng)于scoped_ptr unique_lock<std::mutex> lck(mtx);//相當(dāng)于unique_ptr 更安全 while (que.empty()) { //消費者線程發(fā)現(xiàn)que是空的,通知生產(chǎn)者線程先生產(chǎn)物品 //#1 掛起,進入等待狀態(tài) #2 把互斥鎖mutex釋放 cv.wait(lck); }//如果其他線程執(zhí)行notify了,當(dāng)前線程就會從等待狀態(tài) =》到阻塞狀態(tài) =》但是要獲取互斥鎖才能繼續(xù)向下執(zhí)行 int val = que.front(); que.pop(); cv.notify_all();//通知其它線程我消費完了,趕緊生產(chǎn)吧 cout << "消費者 消費:" << val << "號物品" << endl; return val; } private: queue<int> que; }; //這里模擬生產(chǎn)者生產(chǎn)10個物品,消費者消費10個物品 void producer(Queue* que)//生產(chǎn)者線程 { for (int i = 1; i <= 10; ++i) { que->put(i); std::this_thread::sleep_for(std::chrono::milliseconds(100));//睡眠100毫秒 } } void consumer(Queue* que)//消費者線程 { for (int i = 1; i <= 10; ++i) { que->get(); std::this_thread::sleep_for(std::chrono::milliseconds(100));//睡眠100毫秒 } } int main() { Queue que; //兩個線程共享的隊列 std::thread t1(producer, &que);//開啟生產(chǎn)者線程 std::thread t2(consumer, &que);//開啟消費者線程 //主線程等待兩個子線程都執(zhí)行完再結(jié)束。 t1.join(); t2.join(); return 0; }
3、lock_gard和unique_lock
lock_gard和unique_lock可以看成unique_ptr和scope_ptr之間的關(guān)系,lock_gard和unique_lock做的事情是一樣的,都是在構(gòu)造函數(shù)中國自動執(zhí)行mutex的lock()函數(shù),在析構(gòu)函數(shù)中自動執(zhí)行mutex的unlock()函數(shù)。
lock_gard源碼:
unique_lock源碼:
4、流程分析
- 首先消費者線程拿到互斥鎖,生產(chǎn)者線程沒有拿到互斥鎖,生產(chǎn)者線程處于阻塞狀態(tài);
- 消費者線程發(fā)現(xiàn)que隊列是空的,進入while循環(huán),進入的等待狀態(tài),將互斥鎖釋放(都是由條件變量控制的);
- 此時生產(chǎn)者線程拿到互斥鎖,不進入while循環(huán),生產(chǎn)對象放入que隊列中,notify_all通知其他線程,也就是消費者線程,由等待狀態(tài)進入阻塞狀態(tài), 當(dāng)生產(chǎn)者線程函數(shù)完之后,出了函數(shù)作用域,將mutex互斥鎖釋放了,消費者線程拿到鎖了,阻塞狀態(tài)變?yōu)檫\行狀態(tài),繼續(xù)向下執(zhí)行;
- 消費者線程消費完之后,繼續(xù)通知生產(chǎn)者,我消費完了。
到此這篇關(guān)于C++詳細(xì)分析線程間的同步通信的文章就介紹到這了,更多相關(guān)C++線程同步通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言創(chuàng)建數(shù)組實現(xiàn)函數(shù)init,empty,reverse
這篇文章主要介紹了C語言創(chuàng)建數(shù)組實現(xiàn)函數(shù)init,empty,reverse,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07C++中的局部變量、全局變量、局部靜態(tài)變量、全局靜態(tài)變量的區(qū)別
本文主要介紹了C++中的局部變量、全局變量、局部靜態(tài)變量、全局靜態(tài)變量的區(qū)別。具有很好的參考價值,下面跟著小編一起來看下吧2017-02-02C++使struct對象擁有可變大小的數(shù)組(詳解)
下面小編就為大家?guī)硪黄狢++使struct對象擁有可變大小的數(shù)組(詳解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-12-12詳解如何實現(xiàn)C++虛函數(shù)調(diào)用匯編代碼
多態(tài)是C++中最重要的特性之一,對虛函數(shù)的調(diào)用在C++代碼中是隨處可見的,本篇文章我們詳細(xì)探討一下,感興趣的朋友快來看看吧2021-11-11