【多线程】C++11进行多线程开发 (std::thread)
文章目錄
- 創建線程
- std::thread 類
- 使用join()
- 使用 detach()
- 警惕作用域
- 線程不能復制
- 給線程傳參
- 傳遞指針
- 傳遞引用
- 以類成員函數為線程函數
- 以容器存放線程對象
- 互斥量
- std::mutex
- std::lock_guard
- 條件變量
- call_once
創建線程
C++11 增加了線程以及線程相關的類, 而之前并沒有對并發編程提供語言級別的支持
std::thread 類
使用 std::thread 類來創建線程, 我們需要提供的只是線程函數, 或者線程對象, 同時提供必要的參數
std::thread 表示單個執行的線程, 使用thread 類首先會構造一個線程對象, 然后開始執行線程函數,
使用join()
我們知道, 上例中如果主線程 (main) 先退出, 那些還未完成任務的線程將得不到執行機會, 因為 main 會在執行完調用 exit(), 然后整個進程就結束了, 那它的"子線程" (我們知道線程是平級的, 這里只是, 形象一點) 自然也就 over 了
所以就像上例中, 線程對象調用 join() 函數, join() 會阻塞當前線程, 直到線程函數執行結束, 如果線程有返回值, 會被忽略
使用 detach()
對比于 join(), 我們肯定有不想阻塞當前線程的時候, 這時可以調用 detach(), 這個函數會分離線程對象和線程函數, 讓線程作為后臺線程去執行, 當前線程也不會被阻塞了, 但是分離之后, 也不能再和線程發生聯系了, 例如不能再調用 get_id() 來獲取線程 id 了, 或者調用 join() 都是不行的, 同時也無法控制線程何時結束
#include <thread> void func() {//... }int main() {std::thread t(func);t.detach();// 可以做其他事了, 并不會被阻塞return 0; }程序終止后, 不會等待在后臺執行的其余分離線程, 而是將他們掛起, 并且本地對象被破壞
警惕作用域
std::thread 出了作用域之后就會被析構, 這時如果線程函數還沒有執行完就會發生錯誤, 因此, 要注意保證線程函數的生命周期在線程變量 std::thread 之內
線程不能復制
std::thread 不能復制, 但是可以移動
也就是說, 不能對線程進行復制構造, 復制賦值, 但是可以移動構造, 移動賦值
std::thread 將 = 重載了, 調用 operator= 是移動構造函數, 復制被禁用了,
給線程傳參
傳遞指針
#include <iostream> #include <thread>void func(int* a){//這里是直接修改指針指向的地址中的值,并不是修改形參指針的指向,所以傳指針可以改變實參的值*a += 10; }int main() {int x = 10;std::thread t1(func, &x);t1.join();std::cout << x << std::endl;return 0; }上例代碼, 可以如愿改變 x 的值, 但是看下面的代碼, 當我們傳遞引用時, 卻好像并不能如我們所想:
傳遞引用
#include <iostream> #include <thread>void func(int& a) {a += 10; }int main() {int x = 10;std::thread t1(func, x); //編譯會報錯// std::thread t1(func, std::ref(x)); //正確的寫法t1.join();std::cout << x << std::endl;return 0; }我們想讓 func 函數對 x 進行更新, 但是實際上給線程傳參會以拷貝的形式復制到線程空間, 所以即使是引用, 引用的實際上是新線程堆棧中的臨時值, 為了解決這個問題, 我們需要使用引用包裝器 std::ref()
改成:
std::thread t1(func, std::ref(x));
實際上, 我的編譯器對于std::thread t1(func, x);這段代碼直接給出了編譯錯誤,改成std::thread t1(func, std::ref(x));后就沒問題了…
以類成員函數為線程函數
因為類內成員涉及 this 指針, 就和所需的線程函數參數不同了
#include <iostream> #include <thread>using namespace std;class A { public:void func1() {cout << "here is class A`s func 1" << endl;}static void func2() {cout << "here is class A`s func 2" << endl;}void func3() {thread t1(&A::func1, this); //非靜態成員函數thread t2(A::func2); //靜態成員函數t1.join();t2.join();} };int main() {A a;thread t1(&A::func1, &a); //非靜態成員函數thread t2(A::func2); //靜態成員函數t1.join();t2.join();a.func3(); } 注意的是, 如果我們選擇將成員函數變成靜態的使用, 那我們就不能使用非靜態的成員變量了, 解決辦法也很簡單, 給靜態成員函數傳遞該對象的 this 指針就好了。- 非靜態成員函數需要加上& 符號,并且應該加上類名和::作用符,寫在類外就是std::thread t1(&A::func1, &a); ,寫在類中就需要用this,即thread t1(&A::func1, this);;
- 靜態成員函數不需要加上&,也沒有this。因為靜態成員函數屬于類,不屬于實例對象。即thread t2(A::func2);
可以參考一下我犯過的這個錯誤
以容器存放線程對象
我們可以用容器保存創建的多個線程對象, 而當我們像其中插入元素時, 建議使用 emplace_bcak() 而不是 push_back()。
我們知道 push_back() 會創建一個臨時對象然后拷貝, 當然自從有了移動語意這里出發都是移動, 如下例:
#include <iostream> #include <thread> #include <vector>using namespace std;class A { public:void func1() {cout << "here is class A`s func 1" << endl;}void func3() {tmpThread.push_back(thread(&A::func1, this)); //(1)tmpThread.emplace_back(&A::func1, this); //(2)}vector<thread> tmpThread; };比較上例中 (1) (2)兩處, 明顯發現emplace_back() 比 push_back() 調用形式更加簡潔, 他會自動推導直接根據你給出的參數初始化臨時對象
emplace_back 不會觸發復制構造和移動構造, 他會直接原地構造一個元素
所以使用 emplace_back 更加簡潔效率也更加高
互斥量
std::mutex
mutex 類是保護共享數據, 避免多線程同時訪問的同步原語;
mutex 也不能復制, 他的operator=被禁用。
- lock
上鎖, 若失敗則阻塞 - try_lock
嘗試上鎖, 失敗則返回 - unlock
解鎖
使用時注意死鎖
std::lock_guard
通常不直接使用 mutex, lock_guard 更加安全, 更加方便。
他簡化了 lock/unlock 的寫法, lock_guard 在構造時自動鎖定互斥量, 而在退出作用域時會析構自動解鎖, 保證了上鎖解鎖的正確操作, 正是典型的 RAII 機制
@zhz: 疑問: std::lock_guard<std::mutex> locker(myLock);這句話是鎖定myLock這個互斥量,避免其他線程獲取這個互斥量嗎? 還是說,只鎖住這句話之后的代碼塊,避免別的線程訪問該代碼塊???
- 答:是鎖住這個互斥量。因為一旦有一個線程的某段代碼鎖住了這個互斥量,其他線程就獲取不了這個鎖的權限了。使用同一個互斥量在不同的地方鎖住,是因為這幾個地方代碼肯定會訪問同一個變量(或者說共享內存區域),不然不需要使用鎖。只有在多線程下才需要使用鎖。哪怕只有一個地方使用鎖,還是必要的,因為不同線程都在同一片代碼塊進行寫操作時,也需要加鎖防止同時在這個地方寫造成寫數據混亂。
還有一些其他互斥量, 如std::recursive::mutex 是遞歸型互斥量, 可以讓同一線程重復申請等等, 就不一一介紹了
條件變量
條件變量是C++11 提供的一種用于等待的同步機制, 可以阻塞一到多個線程, 直到收到另一個線程發出的通知或者超時, 才會喚醒當前阻塞的線程, 條件變量需要和互斥量配合起來使用
- std::condition_variable
該條件變量必須配合 std::unique_lock 使用 - std::condition_variable_any
可以和任何帶 lock, unlock 的 mutex 配合使用. 他更加通用, 更加靈活, 但是效率比前者差一些, 使用時會有一些額外的開銷
這兩者具有相同的成員函數
通知
-
notify_one
喚醒一個阻塞于該條件變量的線程。 如果有多個等待的線程, 并沒有會優先喚醒誰的說法。即, 沒有喚醒順序, 是隨機的. -
notify_all
喚醒所有阻塞于該條件變量的線程
等待
- wait
讓當前線程阻塞直至條件變量被通知喚醒 - wait_for
導致當前線程阻塞直至通知條件變量、超過指定時間長度
下面示例中, wait_for()最后一個參數是預制條件,調用 wait_for的時候,首先就會判斷這個條件,- 如果這個條件返回false,那么會繼續等待;
- 如果在超時之前,收到了一個notify,那么他會再次執行這個預制條件來進行判斷,超時的時候也還會再次執行這個條件,這種可以用在處理隊列事件:
原文鏈接:https://blog.csdn.net/najiutan/article/details/110817106
- wait_until
導致當前線程阻塞直至通知條件變量、抵達指定時間點
wait_for需要相對時間(“等待長達10秒”),而wait_until需要絕對時間(“等到2012年10月30日12:00”)。
// wait_for: const std::chrono::duration<Rep, Period>& rel_time// wait_until: const std::chrono::time_point<Clock, Duration>& abs_time
比較時間參數的聲明:關于條件變量的詳細以及wait_for和wait_until用法可參考
因為虛假喚醒的存在 和 為了避免丟失信號量 (避免丟失信號量就是在調用wait的時候, 在其之前發出的喚醒都不會對wait生效, 而系統不會保存這些條件變量, 調用完就丟掉了),
我們必須使用循環判斷條件變量,所以我們使用條件變量必須結合 mutex ,并且將判斷條件放入 while 循環, 而不是使用 if。
std::call_once
<mutex>中還提供了std::call_once函數,保證某個函數即使在多個線程中同時調用時,也只被調用一次。使用 std::call_once 需要同時使用其幫助結構體 once_flag
template< class Callable, class... Args > void call_once( std::once_flag& flag, Callable&& f, Args&&... args );- 如果調用call_once時flag已經被設置,說明函數f已經被調用過了,這種情況下call_once直接返回;
- 如果flag未被設置,則調用call_once時會直接調用std?::?forward<Callable>(f),并向其傳遞std?::?forward<Args>(args)...參數。如果此時f內拋出了異常,則異常會傳遞給call_once的調用者,并且不會設置flag,這樣可以使得后續使用同一標志調用call_once時能繼續調用f函數。
這篇博客算是拖了好幾個月才寫的了, 寫一半還沒了, 以后寫博客記得好好保存…
總結
以上是生活随笔為你收集整理的【多线程】C++11进行多线程开发 (std::thread)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【数据结构与算法】4.数据结构图文解析系
- 下一篇: 【多线程】多线程锁住的是什么、std::