關于多線程的概念與理解,可以先了解Linux下的底層線程。當對底層線程有了一定程度理解以后,再學習語言級別的多線程編程就輕而易舉了。
【Linux】多線程 -> 從線程概念到線程控制
【Linux】多線程 -> 線程互斥與死鎖
語言級別的多線程編程最大的好處就是可以跨平臺,使用語言級別編寫的多線程程序不僅可以在Windows下直接編譯運行,在Linux下也是可以直接編譯運行的。其實本質還是調用了不同操作系統的底層線程API。
多線程簡單使用
- 怎么創建啟動一個線程?
#include<iostream>
#include<thread>
#include<chrono>void threadHandle1(int time)
{// 讓線程休眠兩秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}int main()
{// 創建一個線程對象,傳入線程函數以及線程函數所需要的參數,線程就開始執行了std::thread t1(threadHandle1, 2);// 主線程等待子線程結束,再繼續往下運行t1.join();std::cout << "main thread done!" << std::endl;return 0;
}
主線程要t1.join()等待子線程結束之后才能向后運行。如果不等待,就會異常終止。
- 子線程如何結束?
子線程函數運行完成就結束了。
也可以將線程設置為分離狀態,主線程就可以不用等待子進程執行完再繼續向后運行了。
void threadHandle1(int time)
{// 讓線程休眠兩秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}int main()
{// 創建一個線程對象,傳入線程函數以及線程函數所需要的參數,線程就開始執行了std::thread t1(threadHandle1, 2);// 主線程等待子線程結束,再繼續往下運行// t1.join();// 設置線程為分離狀態t1.detach();std::cout << "main thread done!" << std::endl;return 0;
}
- ?主線程如何處理子線程?
1、t1.join()等待線程,等待線程結束后主線程再繼續向后運行。
2、t1.detach()分離線程,主線程結束,整個進程就結束了,所有線程都自動結束了。
#include<iostream>
#include<thread>
#include<chrono>void threadHandle1(int time)
{// 讓線程休眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}void threadHandle2(int time)
{// 讓線程休眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}int main()
{// 創建一個線程對象,傳入線程函數以及線程函數所需要的參數,線程就開始執行了std::thread t1(threadHandle1, 2);std::thread t2(threadHandle2, 2);// 主線程等待子線程結束,再繼續往下運行t1.join();t2.join();// 設置線程為分離狀態// t1.detach();std::cout << "main thread done!" << std::endl;return 0;
}
?mutex互斥鎖和lock_guard
#include<iostream>
#include<thread>
#include<list>int tickets = 1000;void getTicket(int i)
{// 模擬用戶搶票的行為while (tickets > 0){std::cout << "用戶:" << i << "正在進行搶票!" << tickets << std::endl;tickets--;std::this_thread::sleep_for(std::chrono::microseconds(100));}
}int main()
{std::list<std::thread> list;for (int i = 1; i <= 3; i++){list.push_back(std::thread(getTicket, i));}for (std::thread &t: list){t.join();}return 0;
}
輸出的結果雜亂無章,并且搶票出現負數。
多線程對臨界區的訪問會存在競態條件:臨界區代碼段在多線程環境下執行,隨著線程的調度時許不同,從而產生不同的結果。所以,我們要保證對臨界區的原子操作,是通過對臨界區加鎖完成的。
由于多個線程同時向標準輸出流std::cout輸出信息,會造成輸出混亂,各線程的輸出可能相互穿插。
在多線程環境下,多個線程同時訪問和修改共享資源tickets,會引發數據競爭問題。比如,當一個線程檢查到tickets>0后,在執行tickets--操作之前,另一個線程也可能檢查到tickets>0,進而導致重復售票或者出現負數票數的情況。
++/--操作并不是原子性的,其實是對應三條匯編指令完成的。
- 讀取:從內存中把變量的值讀取到寄存器
- 修改:在寄存器里將變量的值+1/-1
- 寫入:把修改后的值寫入到內存
在單線程環境下,這三個步驟順序執行不會有問題。但是在多線程環境下,多個線程可能對同一個變量同時進行++/--操作,從而導致數據競爭的問題。
可以看下面的過程演示。
一:
二:
三:
C++11是通過加鎖來保證++/--操作的原子性的。
#include<iostream>
#include<thread>
#include<list>
#include<mutex>int tickets = 1000;
std::mutex mtx; // 全局的一把鎖void getTicket(int i)
{// 模擬用戶搶票的行為while (tickets > 0){mtx.lock();// 鎖+雙重判斷,如果不雙重判斷,當tickets為1時,用戶正在搶票還沒有執行到tickets--,其他用戶判斷tickets>0,也進來了等待鎖。// 用戶搶票之后,其他用戶獲取到鎖還會進行搶票。if (tickets > 0){// 臨界區代碼段 - 加鎖保護std::cout << "用戶:" << i << "正在進行搶票!" << tickets << std::endl;tickets--;mtx.unlock();}}
}int main()
{std::list<std::thread> list;for (int i = 1; i <= 3; i++){list.push_back(std::thread(getTicket, i));}for (std::thread &t: list){t.join();}return 0;
}
通過加鎖和雙重判斷的方式,這樣就能做到對多線程對共享資源tickets的安全訪問了。
lock_guard和unique_lock
其實不需要我們手動加鎖和解鎖,因為,如果臨界區內有return,break等語句,此線程獲取鎖,但是在釋放鎖之前break或者renturn了,導致鎖沒有釋放。那么其他線程也獲取不了鎖,就會造成死鎖問題,所以對于這把互斥鎖要有RAII的思想。
使用lock_guard。構造函數獲取鎖,析構函數釋放鎖。
int tickets = 1000;
std::mutex mtx; // 全局的一把鎖void getTicket(int i)
{// 模擬用戶搶票的行為while (tickets > 0){{std::lock_guard<std::mutex> lock(mtx);// mtx.lock();// 鎖+雙重判斷,如果不雙重判斷,當tickets為1時,用戶正在搶票還沒有執行到tickets--,其他用戶判斷tickets>0,也進來了等待鎖。// 用戶搶票之后,其他用戶獲取到鎖還會進行搶票。if (tickets > 0){std::cout << "用戶:" << i << "正在進行搶票!" << tickets << std::endl;tickets--;}// mtx.unlock();}std::this_thread::sleep_for(std::chrono::microseconds(100));}
}
不管臨界區是正常執行,還是break或return了,出了作用域,lock_guard會自動調用析構函數釋放鎖,保證所有線程都能釋放鎖,避免死鎖問題發生。
lock_guard不支持拷貝構造和賦值運算符重載,如果需要拷貝和賦值可以使用unique_lock,支持移動語義,可以使用右值引用的拷貝構造和賦值運算符重載。
int tickets = 1000;
std::mutex mtx; // 全局的一把鎖void getTicket(int i)
{// 模擬用戶搶票的行為while (tickets > 0){{std::unique_lock<std::mutex> lck(mtx);// std::lock_guard<std::mutex> lock(mtx);// mtx.lock();// 鎖+雙重判斷,如果不雙重判斷,當tickets為1時,用戶正在搶票還沒有執行到tickets--,其他用戶判斷tickets>0,也進來了等待鎖。// 用戶搶票之后,其他用戶獲取到鎖還會進行搶票。if (tickets > 0){std::cout << "用戶:" << i << "正在進行搶票!" << tickets << std::endl;tickets--;}// mtx.unlock();}std::this_thread::sleep_for(std::chrono::microseconds(100));}
}
總結:lock_guard不能用在函數傳遞或返回的過程中,因為lock_guard刪除了拷貝構造和賦值,只能用在簡單的臨界區代碼段的互斥操作中。unique_guard可以用在函數傳遞或返回的過程中,因為支持移動語義,可以使用移動構造和移動賦值。unique_guard通常與條件變量一起使用。
線程間的同步通信
生產者-消費者模型
std::mutex mtx;
std::condition_variable cv;// 邊生產邊消費
class Queue
{
public:void put(int val){// 加鎖std::unique_lock<std::mutex> lock(mtx);// 如果隊列不為空,則等待while (!que.empty()){// 1.進入等待狀態 2.釋放鎖cv.wait(lock);// 被喚醒之后,等待狀態進入阻塞狀態,獲取鎖進入就緒狀態繼續執行}que.push(val);cv.notify_all(); // 通知消費者消費std::cout << "生產者 生產:" << val << "號物品" << std::endl;}int get(){// 加鎖std::unique_lock<std::mutex> lock(mtx);// 如果隊列為空,則等待while (que.empty()){// 1.進入等待狀態 2.釋放鎖cv.wait(lock);// 被喚醒之后,等待狀態進入阻塞狀態,獲取鎖進入就緒狀態繼續執行}int val = que.front();que.pop();cv.notify_all(); // 通知生產者生產std::cout << "消費者 消費:" << val << "號物品" << std::endl;return val;}
private:std::queue<int> que;
};// 生產
void producer(Queue* que)
{for (int i = 0; i <= 10; i++){que->put(i);std::this_thread::sleep_for(std::chrono::microseconds(100));}
}
// 消費
void conducer(Queue* que)
{for (int i = 0; i <= 10; i++){que->get();std::this_thread::sleep_for(std::chrono::microseconds(100));}
}
int main()
{Queue que;std::thread p(producer, &que);std::thread c(conducer, &que);p.join();c.join();return 0;
}