文章目錄
- 建立線程
-
- std::thread 類
- 使用join()
- 使用 detach()
- 警惕作用域
- 線程不能複制
- 給線程傳參
-
- 傳遞指針
- 傳遞引用
- 以類成員函數為線程函數
- 以容器存放線程對象
- 互斥量
-
- std::mutex
- std::lock_guard
- 條件變量
- call_once
建立線程
C++11 增加了線程以及線程相關的類, 而之前并沒有對并發程式設計提供語言級别的支援
std::thread 類
使用
std::thread
類來建立線程, 我們需要提供的隻是線程函數, 或者線程對象, 同時提供必要的參數
std::thread
表示單個執行的線程, 使用
thread
類首先會構造一個線程對象, 然後開始執行線程函數,
#include <iostream>
#include <thread> //需要包含的頭
using namespace std;
void func(int a, double b) //有參數, 參數數量不限
{
cout << a << ' ' << b << endl;
}
void func2() //無參數
{
cout << "hello!\n";
}
int main()
{
thread t1(func, 1, 2); //提供參數
thread t2(func2);
//可以使用 lambda表達式
thread t3([](int a, double b){cout << a << ' ' << b << endl;}, 3, 4);
cout << t1.get_id() << "****" << endl; //可以使用 get_id() 擷取線程 id
t1.join();
t2.join();
t3.join();
return 0;
}
使用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
不能複制, 但是可以移動
也就是說, 不能對線程進行複制構造, 複制指派, 但是可以移動構造, 移動指派
#include <iostream>
#include <thread>
void func()
{
std::cout << "here is func" << std::endl;
}
int main()
{
std::thread t1(func);
std::thread t2;
t2 = t1; //error
t2 = std::move(t1); //right, 将 t1 的線程控制權轉移給 t2
std::cout << t1.get_id() << std::endl; //error,t1已經失去了線程控制權
t1 = std::thread(func); //right, 直接構造, 建立的是臨時對象,是以隐式調用move
t1 = std::move(t2); //error, 不能通過指派一個新值來放棄一個已有線程, 這樣會直接導緻程式崩潰
}
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
機制
#include <thread>
#include <mutex>
std::mutex myLock;
void func()
{
{
std::lock_guard<std::mutex> locker(myLock); //出作用域自動解鎖
//do some things...
}
myLock.lock();
myLock.unlock();
}
int main()
{
std::thread t(func);
t.join();
}
@zhz: 疑問:
std::lock_guard<std::mutex> locker(myLock);
這句話是鎖定
myLock
這個互斥量,避免其他線程擷取這個互斥量嗎? 還是說,隻鎖住這句話之後的代碼塊,避免别的線程通路該代碼塊???
- 答:是鎖住這個互斥量。因為一旦有一個線程的某段代碼鎖住了這個互斥量,其他線程就擷取不了這個鎖的權限了。使用同一個互斥量在不同的地方鎖住,是因為這幾個地方代碼肯定會通路同一個變量(或者說共享記憶體區域),不然不需要使用鎖。隻有在多線程下才需要使用鎖。哪怕隻有一個地方使用鎖,還是必要的,因為不同線程都在同一片代碼塊進行寫操作時,也需要加鎖防止同時在這個地方寫造成寫資料混亂。
還有一些其他互斥量, 如 std::recursive::mutex
是遞歸型互斥量, 可以讓同一線程重複申請等等, 就不一一介紹了
條件變量
條件變量是C++11 提供的一種用于等待的同步機制, 可以阻塞一到多個線程, 直到收到另一個線程發出的通知或者逾時, 才會喚醒目前阻塞的線程, 條件變量需要和互斥量配合起來使用
-
該條件變量必須配合std::condition_variable
使用std::unique_lock
-
可以和任何帶 lock, unlock 的 mutex 配合使用. 他更加通用, 更加靈活, 但是效率比前者差一些, 使用時會有一些額外的開銷std::condition_variable_any
這兩者具有相同的成員函數
通知
-
喚醒一個阻塞于該條件變量的線程。 如果有多個等待的線程, 并沒有會優先喚醒誰的說法。即, 沒有喚醒順序, 是随機的.notify_one
-
喚醒所有阻塞于該條件變量的線程notify_all
等待
-
讓目前線程阻塞直至條件變量被通知喚醒wait
-
導緻目前線程阻塞直至通知條件變量、超過指定時間長度wait_for
- 下面示例中,
最後一個參數是預制條件,調用wait_for()
的時候,首先就會判斷這個條件,wait_for
- 如果這個條件傳回
,那麼會繼續等待;false
- 如果在逾時之前,收到了一個
,那麼他會再次執行這個預制條件來進行判斷,逾時的時候也還會再次執行這個條件,這種可以用在處理隊列事件:notify
原文連結:https://blog.csdn.net/najiutan/article/details/110817106// wait_for的第一個參數是鎖,第二個參數是時間長度,第三個參數是lambda表達式 cond_var.wait_for(lck, std::chrono::seconds(20), [] { std::this_thread::sleep_for(std::chrono::seconds(1)); return false; });
- 如果這個條件傳回
-
導緻目前線程阻塞直至通知條件變量、抵達指定時間點wait_until
-
需要相對時間(“等待長達10秒”),而wait_for
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
這篇部落格算是拖了好幾個月才寫的了, 寫一半還沒了, 以後寫部落格記得好好儲存…#include <iostream> #include <thread> #include <mutex> using namespace std; once_flag onlyOnce; mutex myMutex; void func() //線程函數 { myMutex.lock(); cout << "here is func" << endl; myMutex.unlock(); call_once(onlyOnce, []{ //僅僅調用一次 cout << "hello world!" << endl; }); } int main() { thread t1(func); thread t2(func); thread t3(func); t1.join(); t2.join(); t3.join(); return 0; }
- 如果調用