前言:
C++ 11通過標準庫引入了對多執行緒的支持,這個是c++的新特性之一,也就是說我們直接用即可,使得C++在并行編程時不需要依賴第三方庫,而且在原子操作中還引入了原子類的概念(這個后文會講到),執行緒啥的就不再解釋了,直接上干貨;
頭檔案一定記得寫如下幾個:
#include <thread> //執行緒庫
#include <condition_variable> //條件變數
#include <mutex> //互斥鎖
1. 執行緒庫函式的使用:
| 函式 | 功能 |
|---|---|
| thread() | 構造一個執行緒物件,沒有關聯任何執行緒函式,即沒有啟動任何執行緒 |
| thread(fn,args1, args2,…) | 構造一個執行緒物件,并關聯執行緒函式fn,args1,args2,…為執行緒函式的引數 |
| get_id() | 獲取執行緒id |
| jionable() | 執行緒是否是有效的,joinable代表的是一個正在執行中的執行緒, |
| join() | 該函式呼叫后會阻塞住執行緒,當該執行緒結束后,主執行緒繼續執行 |
| detach() | 在創建執行緒物件后馬上呼叫,用于把被創建執行緒與主執行緒分離,分離的執行緒變為后臺執行緒,創建的執行緒的"死活"就與主執行緒無關, |
注意:
- 執行緒物件可以關聯一個執行緒,用來控制執行緒以及獲取執行緒的狀態,
- 當創建一個執行緒物件后,沒有提供執行緒函式,該物件實際沒有對應任何執行緒,
- 執行緒函式的引數是以值拷貝的方式拷貝到執行緒堆疊空間中的,其實際參考的是執行緒堆疊中的拷貝,而不是外部實參,(也就是說創建thread物件進行系結時,哪怕你的形參事參考都不會改變當前函式的變數值,有例子)
- 一個執行緒物件只能使用一次join(),不然程式會崩潰;在執行緒物件銷毀前,要么以jion()的方式等待執行緒結束,要么以detach()的方式,
樣例1(對應上面第三點):
void Fun1(int& x)
{
x += 20;
}
void Fun2(int* x)
{
*x += 20;
}
int main()
{
int a = 10;
thread t1(Fun1, a);
t1.join();
//執行緒函式引數盡管是參考方式,實際參考的是執行緒堆疊中的拷貝
cout << a << endl; // 10
// 如果想要通過形參改變外部實參時,怎么辦呢?這時借助std::ref()函式
thread t3(Fun1, std::ref(a));
t3.join();
cout << a << endl; //30
thread t2(Fun2, &a);
t2.join();
cout << a << endl; //50
return 0;
}
2. 原子操作
C++11標準定義“原子型別”,可以保證原子型別在執行緒間被互斥的訪問,
atomic_bool abool; //對應bool
atomic_char achar; //char
atomic_schar aschar; //signed char
atomic_uchar auchar; //unsigned char
atomic_int aint; //int
atomic_uint auint; //unsigned int
atomic_short ashort; //short
atomic_ushort aushort; //unsigned short
atomic_long along; //long
atomic_ulong aulong; //unsigned long
atomic_llong allong; //long long
atomic_ullong aullong; //unsigned long long
atomic_char16_t achar16_t; //char16_t
atomic_char32_t achar32_t; //char32_t;
atomic_wchar_t awchar_t; //wchar_t
但是,我們應該使用atomic類模板,通過該模板,可以定義出任意需要的原子型別:
std::atomic< type > t;
對執行緒而言,原子型別通常屬于“資源型”的資料,這意味著多個執行緒通常只能訪問的原子型別的拷貝,所以在C++11中,原子型別只能從其模板引數型別中進行構造,標準不允許原子型別經行拷貝構造、移動構造,以及operator=等,防止以外發生;
舉個例子:
atomic< float > af{ 1.2f };
//atomic< float > af1{ af }; //這里無法編譯
原因:atomic模板類的拷貝建構式、移動建構式、operator=等總是默認被洗掉的
在C++11中,標準將原子操作定義為atomic模板類的成員函式,這囊括了絕大多數典型的操作,如讀、寫、交換等,當然,對于內置型別而言,主要是通過多載一些全域運算子來完成的,在編譯的時候,會產生一條特殊的lock前綴的x86指令,lock能夠控制總線及實作x86平臺上的原子性,

上面的那些是原子型別的函式的操作:讀(load)、寫(store)、交換(exchange)、比較并交換(compare_exchange_weak/compare_exchange_stronge)等操作;
當然,有時編譯器會給我們作出優化:
atomic<int> a;
a = 1; //a.store(1);
int b = a; //b = a.load();
上圖中,那個atomic_flag,這個要特別關注一下,聽說效率很高,可以自制自旋鎖,如下:
void Lock(atomic_flag *lock) { while (lock->test_and_set()); }
void Ublock(atomic_flag *lock) { lock->clear(); }
//test_and_set()函式是設定true值,回傳之前的值,
//clear()是復位,置為false;
std::atomic_flag lock = ATOMIC_FLAG_INIT; //初始化
代碼演示:
// 自旋鎖實作.cpp : 定義控制臺應用程式的入口點,
//
#include <iostream>
#include <atomic>
#include <thread>
#include <Windows.h>
using namespace std;
std::atomic_flag lock = ATOMIC_FLAG_INIT; //宣告了全域變數,初始化為值ATOMIC_FLAG_INIT,即false狀態
void Lock(atomic_flag *lock) { while (lock->test_and_set()){
cout<<"Waiting..."<<endl;
} }
void Ublock(atomic_flag *lock) { lock->clear(); }
void func(){
Lock(&lock);
cout << "func working..." << endl;
Ublock(&lock);
}
void foo(){
Lock(&lock);
cout<<"foo working..."<<endl;
Ublock(&lock);
}
int main(void)
{
std::thread t1(func);
std::thread t2(foo);
t1.join();
t2.join();
system("pause");
return 0;
}
截圖:

原子型別有一些列舉值,這個可以稍微了解一下,

高級用法:
一、多執行緒啟動函式:std::async()
宣告方式:
template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
async (launch policy, Fn&& fn, Args&&... args);
其中:
// 異步啟動的策略
enum class launch {
// 異步啟動,在呼叫std::async()時創建一個新的執行緒以異步呼叫函式,并回傳future物件;
async = 0x1,
// 延遲啟動,在呼叫std::async()時不創建執行緒,直到呼叫了future物件的get()或wait()方法時,才創建執行緒;
deferred = 0x2,
// 自動,函式在某一時刻自動選擇策略,這取決于系統和庫的實作,通常是優化系統中當前并發的可用性
any = async | deferred,
sync = deferred
};
//引數 fn 是要呼叫的可呼叫 (Callable) 物件
//引數args 是傳遞給 f 的引數
//std::launch::async:在呼叫async就開始創建執行緒,
//std::launch::deferred:延遲加載方式創建執行緒,呼叫async時不創建執行緒,直到呼叫了future的get或者wait時才創建執行緒,
異步呼叫,當然可以大大提高程式運行的效率~
std::async()是一個接受回呼(函式或函式物件)作為引數的函式模板,通過啟動一個新執行緒或者復用一個它認為合適的已有執行緒異步呼叫,
std::async回傳一個std::future< T >,它存盤由std::async()執行的函式物件回傳的值,所以通常都有std::future伴隨著使用,因為future中存盤了執行緒函式回傳的結果,
內部原理:
std::async先異步操作用std::packaged_task包裝執行緒函式,然后將異步操作的結果放到std::promise中,最后再通過future.get/wait來獲取這個未來的結果,(后面會講到)
二、std::future
std::future提供了一種訪問異步操作結果的機制,也就是我們可以通過這個類物件,異步訪問被呼叫執行緒函式的結果;
是類模板:
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
獲取future結果有三種方式:
- get:等待異步操作結束并回傳結果(會阻塞當前呼叫函式)
- wait:等待異步操作完成,沒有回傳值 (同上)
- wait_for是超時等待回傳結果,
也可以通過查詢future的狀態(future_status)來獲取異步操作的結果,future_status有三種狀態:
- deferred:異步操作還沒開始
- ready:異步操作已經完成
- timeout:異步操作超時
例子:
//查詢future的狀態
std::future_status status;
do {
status = future.wait_for(std::chrono::seconds(1)); //等待一秒
if (status == std::future_status::deferred) {
std::cout << "deferred\n";
} else if (status == std::future_status::timeout) {
std::cout << "timeout\n";
} else if (status == std::future_status::ready) {
std::cout << "ready!\n";
}
} while (status != std::future_status::ready);
std::chrono知識點
代碼演示:
# include <iostream>
# include <ctime>
# include <future>
# include <thread>
using namespace std;
int funca(int a,int b){
return a+b;
}
int funcb(int a){
return a;
}
int main(void)
{
future<int> f1 = std::async(funca,1,2); //<type> 是系結的函式回傳值
future<int> f2 = std::async(funcb,3);
auto it = f1.get() + f2.get();
cout<<it<<endl;
system("pause");
return 0;
}
三、std::promise
std::promise可以獲取執行緒函式里的值,不過要等執行完畢后才可以獲取;當然,是間接地通過promise內部提供的future來獲取的!
用法:
std::promise<int> pr;
std::thread t([](std::promise<int>& p){ p.set_value_at_thread_exit(9); },std::ref(pr));
std::future<int> f = pr.get_future();
點擊了解
四、std::packaged_task
這個是包裝了一個可呼叫物件(如function, lambda expression, bind expression, or another function object);packaged_task保存的是一個函式,
用法:
std::packaged_task<int()> task([](){ return 7; });
std::thread t1(std::ref(task));
std::future<int> f1 = task.get_future();
auto r1 = f1.get();
注:一般來說,用std::future以及std::async這兩個用法即可,
鎖
鎖: 最常見的就是mutex (有RAII思想的管理鎖的類模板,可以預防我們忘記解鎖)
C++11根據mutext的屬性提供四種的互斥量,分別是:
- std::mutex,最常用,普遍的互斥量(默認屬性),
- std::recursive_mutex ,遞回鎖,允許同一執行緒使用recursive_mutext多次加鎖,然后使用相同次數的解鎖操作解鎖,mutex多次加鎖會造成死鎖
- std::timed_mutex,在mutex上增加了時間的屬性,增加了兩個成員函式try_lock_for(),try_lock_until(),分別接收一個時間范圍,再給定的時間內如果互斥量被鎖主了,執行緒阻塞,超過時間,回傳false,
- std::recursive_timed_mutex,增加遞回和時間屬性

時間鎖:
timed_mutex myMutex;
chrono::milliseconds timeout(1000); //1秒
if (myMutex.try_lock_for(timeout))
{
//在1秒內獲取了鎖
//業務代碼
myMutex.unlock();
}
else
{
//在100毫秒內沒有獲取鎖
//業務代碼
}
time_mutex博客
mutex成員函式(常用):
6. lock(),互斥量加鎖,如果互斥量已被加鎖,執行緒阻塞
7. bool try_lock(),嘗試加鎖,如果互斥量未被加鎖,則執行加鎖操作,回傳true;如果互斥量已被加鎖,回傳false,執行緒不阻塞,
8. void unlock(),解鎖互斥量

mutex RAII式的加鎖解鎖
std::lock_guard:
管理mutex的類,以獨占所有權的方式管理mutex物件的上鎖和解鎖操作,物件構建時傳入mutex,會自動對mutex加入,直到離開類的作用域,析構時完成解鎖,RAII式的堆疊物件能保證在例外情形下mutex可以在lock_guard物件析構被解鎖,

原始碼:
template<class _Mutex>
class lock_guard
{
public:
// 在構造lock_gard時,_Mtx還沒有被上鎖
explicit lock_guard(_Mutex& _Mtx)
: _MyMutex(_Mtx)
{
_MyMutex.lock();
}
// 在構造lock_gard時,_Mtx已經被上鎖,此處不需要再上鎖
lock_guard(_Mutex& _Mtx, adopt_lock_t)
: _MyMutex(_Mtx)
{}
~lock_guard()
{
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
例子:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mut;
void Print(int num)
{
std::cout << "this is thread_unlock: " <<num<< std::endl;
{
std::lock_guard<std::mutex> lg(mut);//初始化就上鎖
std::cout << "this is thread: " << num << std::endl;
}//離開塊作用域就自動解鎖
}
int main()
{
std::thread t1(Print, 1);
std::thread t2(Print, 2);
t1.join();
t2.join();
std::cout << "this is main thread " << std::endl;
return 0;
}
std::unique_lock:
也是以獨占所有權的方式管理mutex物件的上鎖和解鎖操作,即其物件之間不能發生拷貝,這個比較靈活,可以讓我們指定“何時”以及“如何”鎖定和結果Mutex,有挺多函式給我們進行選擇,

總述:

條件變數(必須先加鎖)
頭檔案:# include < condition_variable >
std::condition_variable readyCondVar;
條件的檢測是在互斥鎖的保護下進行的,執行緒在改變條件狀態之前必須首先鎖住互斥量,如果條件為假,這個執行緒自動阻塞,并釋放等待狀態改變的互斥鎖,如果另一個執行緒改變了條件,它發信號給關聯的條件變數,喚醒一個或多個等待它的執行緒,重新獲得互斥鎖,重新評價條件,如果兩行程共享可讀寫的記憶體,條件變數 可以被用來實作這兩行程間的執行緒同步,
常用API介面:

代碼:
std::mutex mutex;
std::condition_variable cv;
// 條件變數與臨界區有關,用來獲取和釋放一個鎖,因此通常會和mutex聯用,
std::unique_lock lock(mutex); //和RALL鎖機制使用
// 此處會釋放lock,然后在cv上等待,直到其它執行緒通過cv.notify_xxx來喚醒當前執行緒,
// cv被喚醒后會再次對lock進行上鎖,然后wait函式才會回傳,
// wait回傳后可以安全的使用mutex保護的臨界區內的資料,此時mutex仍為上鎖狀態
cv.wait(lock)
(上面的wait這個函式可能會導致驚群效應,所以我們可以用多載版本,cv.wait(lock,可呼叫函式物件));
類似這樣:
> g_cv.wait(lock, [] ){ return xxx; });
參考此篇文章:請點擊!
notify_one()與notify_all()
點擊鏈接查看這個知識點,(這篇博客也不嚴謹,我是持懷疑態度,,,)
參考文章:
- 博客一
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/277802.html
標籤:其他
上一篇:Java集合型別【學習筆記】
