C++線程間的互斥和通信場景分析
互斥鎖(mutex)
為了更好地理解,互斥鎖,我們可以首先來看這么一個應用場景:模擬車站賣票。
模擬車站賣票
場景說明:
Yang車站售賣從亞特蘭蒂斯到古巴比倫的時光飛船票;因為機會難得,所以票數(shù)有限,一經(jīng)發(fā)售,謝絕補票。
飛船票總數(shù):100張;
售賣窗口:3個。
對于珍貴的飛船票來說,這個資源是互斥的,比如第100張票,只能賣給一個人,不可能同時賣給兩個人。3個窗口都有權限去售賣飛船票(唯一合法途徑)。
不加鎖的結果
根據(jù)場景說明,我們可以很快地分析如下:
可以使用三個線程來模擬三個獨立的窗口同時進行賣票;
定義一個全局變量,每當一個窗口賣出一張票,就對這個變量進行減減操作。
故寫出如下代碼:
#include <iostream> #include <thread> #include <list> using namespace std; int tickets = 100; // 車站剩余票數(shù)總數(shù) void sellTickets(int win) { while (tickets > 0) { { if (tickets > 0) { cout << "窗口:" << win << " 賣出了第:" << tickets << "張票!" << endl; tickets--; } std::this_thread::sleep_for(std::chrono::microseconds(400)); } } } int main() { list<std::thread> tlist; for (int i = 1; i <= 3; ++i) { tlist.push_back(std::thread(sellTickets, i)); } for (std::thread& t : tlist) { t.join(); } cout << "所有窗口賣票結束!" << endl; return 0; }
運行結果如下:
通過運行,我們可以發(fā)現(xiàn)問題:
對于一張票來說,賣出去了多次!
這不白嫖嗎???這合適嗎?
原因也很簡單,對于線程來說,誰先執(zhí)行,誰后執(zhí)行,完全是根據(jù)CPU的調(diào)度,根本不可能掌握清楚。
所以,這個代碼是線程不安全
的!
那,怎么解決呢?
當然是:互斥鎖了!
加鎖后的結果
我們對上述代碼做出如下修改:
#include <iostream> #include <thread> #include <list> #include <mutex> using namespace std; int tickets = 100; std::mutex mtx; void sellTickets(int win) { while (tickets > 0) { { lock_guard<std::mutex> lock(mtx); if (tickets > 0) { cout << "窗口:" << win << " 賣出了第:" << tickets << "張票!" << endl; tickets--; } std::this_thread::sleep_for(std::chrono::microseconds(400)); } } } int main() { list<std::thread> tlist; for (int i = 1; i <= 3; ++i) { tlist.push_back(std::thread(sellTickets, i)); } for (std::thread& t : tlist) { t.join(); } cout << "所有窗口賣票結束!" << endl; return 0; }
首先定義了一個全局的互斥鎖std::mutex mtx
;接著在對票數(shù)tickets
進行減減操作時,定義了lock_guard
,這個就相當于智能指針scoped_ptr
一樣,可以出了作用域自動釋放鎖資源。
運行結果如下:
我們可以看到這一次,就沒問題了。
簡單總結
互斥鎖的使用可以有三種:
(首先都需要在全局定義互斥鎖std::mutex mtx
)
- 首先可以直接在需要加鎖和解鎖的地方,手動進行:加鎖
mtx.lock()
、解鎖mtx.unlock()
; - 可以在需要加鎖的地方定義保護鎖:
lock_guard<std::mutex> lock(mtx)
,這個鎖在定義的時候自動上鎖,出了作用域自動解鎖。(其實就是借助了智能指針
的思想,定義對象出調(diào)用構造函數(shù)底層調(diào)用lock()
,出了作用域調(diào)用析構函數(shù)底層調(diào)用unlock()
); - 可以在需要加鎖的地方定義唯一鎖:
unique_lock<std::mutex> lock(mtx)
,這個鎖和保護鎖類似,但是比保護鎖更加好用。(可以類比智能指針中的scoped_ptr
和unique_ptr
的區(qū)別,二者都是將拷貝構造和賦值重載函數(shù)刪除了,但是unique_ptr
和unique_lock
都定義了帶有右值引用的拷貝構造和賦值)
條件變量(conditon_variable)
如果說,互斥鎖
是為了解決線程間互斥
的問題,那么,條件變量就是為了解決線程間通信
的問題。
同樣的,我們可以首先來看一個問題(模型):
生產(chǎn)者消費者線程模型
生產(chǎn)者消費者線程模型是一個很經(jīng)典的線程模型;
首先會有兩個線程,一個是生產(chǎn)者,一個是消費者,生產(chǎn)者只負責生產(chǎn)資源,消費者只負責消費資源。
產(chǎn)生問題
根據(jù)上述互斥鎖的理解,我們可以寫出如下代碼:
#include <iostream> #include <thread> #include <mutex> #include <queue> using namespace std; std::mutex mtx; class Queue { public: void put(int num) { lock_guard<std::mutex> lock(mtx); que.push(num); cout << "生產(chǎn)者,生產(chǎn)了:" << num << "號產(chǎn)品" << endl; } void get() { lock_guard<std::mutex> lock(mtx); int val = que.front(); que.pop(); cout << "消費者,消費了:" << val << "號產(chǎn)品" << endl; } private: queue<int> que; }; void producer(Queue* que) { for (int i = 0; i < 10; ++i) { que->put(i); std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } void consumer(Queue* que) { for (int i = 0; i < 10; ++i) { que->get(); std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } int main() { Queue que; std::thread t1(producer, &que); std::thread t2(consumer, &que); t1.join(); t2.join(); return 0; }
同樣的,我們定義了兩個線程:t1
、t2
分別作為生產(chǎn)者
和消費者
,并且定義了兩個線程函數(shù)
:producer
和consumer
,這兩個函數(shù)接受一個Queue*
的參數(shù),并且通過這個指針調(diào)用put
和get
方法,這兩個方法就是往資源隊列里面執(zhí)行入隊和出隊操作。
運行結果如下:
我們會發(fā)現(xiàn),出錯了。
多運行幾次試試:
我們發(fā)現(xiàn),每次運行的結果還都不一樣,但是都會出現(xiàn)系統(tǒng)崩潰的問題。
仔細來看這個錯誤原因:
我們再想想這個代碼的邏輯:
一個生產(chǎn)者只負責生產(chǎn);
一個消費者只負責消費;
他們共同在隊列里面存取資源;
存取資源操作本身是互斥的。
發(fā)現(xiàn)問題了嗎?
這兩個線程之間彼此的操作獨立,換句話說,
沒有通信!
生產(chǎn)者生產(chǎn)的時候,消費者不知道;
消費者消費的時候,生產(chǎn)者也不知道;
但是消費者是要從隊列里面取資源
的,如果某一個時刻,隊列里為空了,它就不能取了!
解決問題
分析完問題之后,我們知道了:
問題出在:沒有通信上面。
那么如何解決通信
問題呢?
當然就是:條件變量
了!
我們做出如下代碼的修改:
#include <iostream> #include <thread> #include <mutex> #include <queue> #include <condition_variable> using namespace std; std::mutex mtx; // 互斥鎖,用于線程間互斥 std::condition_variable cv;// 條件變量,用于線程間通信 class Queue { public: void put(int num) { unique_lock<std::mutex> lck(mtx); while (!que.empty()) { cv.wait(lck); } que.push(num); cv.notify_all(); cout << "生產(chǎn)者,生產(chǎn)了:" << num << "號產(chǎn)品" << endl; } void get() { unique_lock<std::mutex> lck(mtx); while (que.empty()) { cv.wait(lck); } int val = que.front(); que.pop(); cv.notify_all(); cout << "消費者,消費了:" << val << "號產(chǎn)品" << endl; } private: queue<int> que; }; void producer(Queue* que) { for (int i = 0; i < 10; ++i) { que->put(i); std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } void consumer(Queue* que) { for (int i = 0; i < 10; ++i) { que->get(); std::this_thread::sleep_for(std::chrono::milliseconds(200)); } } int main() { Queue que; std::thread t1(producer, &que); std::thread t2(consumer, &que); t1.join(); t2.join(); return 0; }
這個時候我們再來看運行結果:
這個時候就是:
生產(chǎn)一個、消費一個。
原子類型(atomic)
我們前面遇到線程不安全的問題,主要是因為涉及++
、--
操作的時候,有可能被其他的線程干擾,所以使用了互斥鎖
。
只允許得到鎖
的線程進行操作;
其他沒有得到鎖
的線程只能眼巴巴的干看著。
但是,對于互斥鎖來說,它是比較重的,它對于臨界區(qū)代碼做的事情比較復雜。
簡單來說,如果只是為了++
、--
這樣的簡單操作互斥的話,使用互斥鎖,就有點殺雞用牛刀的意味了。
那么有沒有比互斥鎖
更加輕量的,并且能夠解決問題的呢?
當然有,就是我們要說的原子類型
。
簡單使用
我們可以簡單設置一個場景:
定義十個線程,對一個公有的變量myCount
進行task
的操作,該操作是對變量進行100次的++
。
所以,如果順利,我們會最終得到myCount = 1000
。
代碼如下:
#include <iostream> #include <thread> #include <atomic> #include <list> volatile std::atomic_bool isReady = false; volatile std::atomic_int myCount = 0; void task() { while (!isReady) { // 線程讓出當前的CPU時間片,等待下一次調(diào)度 std::this_thread::yield(); } for (int i = 0; i < 100; ++i) { myCount++; } } int main() { std::list<std::thread> tlist; for (int i = 0; i < 10; ++i) { tlist.push_back(std::thread(task)); } std::this_thread::sleep_for(std::chrono::milliseconds(200)); isReady = true; for (std::thread& it : tlist) { it.join(); } std::cout << "myCount:" << myCount << std::endl; return 0; }
運行結果如下:
改良車站賣票
對于原子類型來說,使用方法非常簡單:
首先包含頭文件:#include <atomic>
;
接著把需要原子操作的變量定義為對應的原子類型就好:
bool
-> atomic_bool
;
int
-> atomic_int
;
其他同理。
理解了這個以后,我們可以使用原子類型對我們的車站賣票進行改良:
#include <iostream> #include <thread> #include <list> #include <mutex> #include <atomic> using namespace std; std::atomic_int tickets = 100; // 車站剩余票數(shù)總數(shù) void sellTickets(int win) { while (tickets > 0) { tickets--; cout << "窗口:" << win << " 賣出了第:" << tickets << "張票!" << endl; } } int main() { list<std::thread> tlist; for (int i = 1; i <= 3; ++i) { tlist.push_back(std::thread(sellTickets, i)); } for (std::thread& t : tlist) { t.join(); } cout << "所有窗口賣票結束!" << endl; return 0; }
可以看到,從代碼長度來說就輕量
了很多!
運行結果如下:
雖然還有部分打印亂序的情況:
(畢竟線程的執(zhí)行順序誰也摸不清 😦 )
但是,代碼的邏輯沒有問題!
不會出現(xiàn)一張票被賣了多次的情況!
這個原子類型也被叫做:無鎖類型,像是一些無鎖隊列
之類的實現(xiàn),就是靠的這個東西。
以上就是C++線程間的互斥和通信的詳細內(nèi)容,更多關于C++線程間通信的資料請關注腳本之家其它相關文章!
相關文章
C++實現(xiàn)LeetCode(188.買賣股票的最佳時間之四)
這篇文章主要介紹了C++實現(xiàn)LeetCode(188.買賣股票的最佳時間之四),本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-08-08