close

c++11關於並發引入了好多好東西,這裡按照如下順序介紹:

std::thread 相關

std::mutex 相關

std::lock 相關

std::atomic 相關

std::call_once 相關

volatile 相關

std::condition_variable 相關

std::future 相關

async 相關

1
std::thread 相關

c++11之前你可能使用 pthread_xxx 來創建線程,繁瑣且不易讀,c++11 引入了 std::thread 來創建線程,支持對線程 join 或者 detach 。直接看代碼:

#include<iostream>#include<thread>usingnamespacestd;intmain(){autofunc=[](){for(inti=0;i<10;++i){cout<<i<<"";}cout<<endl;};std::threadt(func);if(t.joinable()){t.detach();}autofunc1=[](intk){for(inti=0;i<k;++i){cout<<i<<"";}cout<<endl;};std::threadtt(func1,20);if(tt.joinable()){//檢查線程可否被jointt.join();}return0;}

上述代碼中,函數 func 和 func1 運行在線程對象 t 和 tt 中,從剛創建對象開始就會新建一個線程用於執行函數,調用 join 函數將會阻塞主線程,直到線程函數執行結束,線程函數的返回值將會被忽略。

如果不希望線程被阻塞執行,可以調用線程對象的detach函數,表示將線程和線程對象分離。

如果沒有調用 join 或者 detach 函數,假如線程函數執行時間較長,此時線程對象的生命周期結束調用析構函數清理資源,這時可能會發生錯誤。

這裡有兩種解決辦法,一個是調用join(),保證線程函數的生命周期和線程對象的生命周期相同,另一個是調用detach(),將線程和線程對象分離。

這裡需要注意,如果線程已經和對象分離,那我們就再也無法控制線程什麼時候結束了,不能再通過join來等待線程執行完。

這裡可以對thread進行封裝,避免沒有調用join或者detach可導致程序出錯的情況出現:

classThreadGuard{public:enumclassDesAction{join,detach};ThreadGuard(std::thread&&t,DesActiona):t_(std::move(t)),action_(a){};~ThreadGuard(){if(t_.joinable()){if(action_==DesAction::join){t_.join();}else{t_.detach();}}}ThreadGuard(ThreadGuard&&)=default;ThreadGuard&operator=(ThreadGuard&&)=default;std::thread&get(){returnt_;}private:std::threadt_;DesActionaction_;};intmain(){ThreadGuardt(std::thread([](){for(inti=0;i<10;++i){std::cout<<"threadguard"<<i<<"";}std::cout<<std::endl;}),ThreadGuard::DesAction::join);return0;}

c++11還提供了獲取線程id,或者系統cpu個數,獲取thread native_handle,使得線程休眠等功能。

std::threadt(func);cout<<"當前線程ID"<<t.get_id()<<endl;cout<<"當前cpu個數"<<std::thread::hardware_concurrency()<<endl;autohandle=t.native_handle();//handle可用於pthread相關操作std::this_thread::sleep_for(std::chrono::seconds(1));
2
std::mutex 相關

std::mutex 是一種線程同步的手段,用於保存多線程同時操作的共享數據。

mutex 分為四種:

std::mutex:獨占的互斥量,不能遞歸使用,不帶超時功能;

std::recursive_mutex:遞歸互斥量,可重入,不帶超時功能;

std::timed_mutex:帶超時的互斥量,不能遞歸;

std::recursive_timed_mutex:帶超時的互斥量,可以遞歸使。

拿一個 std::mutex 和 std::timed_mutex 舉例吧,別的都是類似的使用方式:

std::mutex:

#include<iostream>#include<mutex>#include<thread>usingnamespacestd;std::mutexmutex_;intmain(){autofunc1=[](intk){mutex_.lock();for(inti=0;i<k;++i){cout<<i<<"";}cout<<endl;mutex_.unlock();};std::threadthreads[5];for(inti=0;i<5;++i){threads[i]=std::thread(func1,200);}for(auto&th:threads){th.join();}return0;}

std::timed_mutex:

#include<iostream>#include<mutex>#include<thread>#include<chrono>usingnamespacestd;std::timed_mutextimed_mutex_;intmain(){autofunc1=[](intk){timed_mutex_.try_lock_for(std::chrono::milliseconds(200));for(inti=0;i<k;++i){cout<<i<<"";}cout<<endl;timed_mutex_.unlock();};std::threadthreads[5];for(inti=0;i<5;++i){threads[i]=std::thread(func1,200);}for(auto&th:threads){th.join();}return0;}
3
std::lock 相關

這裡主要介紹兩種RAII方式的鎖封裝,可以動態的釋放鎖資源,防止線程由於編碼失誤導致一直持有鎖。

c++11主要有 std::lock_guard 和 std::unique_lock 兩種方式,使用方式都類似,如下:

#include<iostream>#include<mutex>#include<thread>#include<chrono>usingnamespacestd;std::mutexmutex_;intmain(){autofunc1=[](intk){//std::lock_guard<std::mutex>lock(mutex_);std::unique_lock<std::mutex>lock(mutex_);for(inti=0;i<k;++i){cout<<i<<"";}cout<<endl;};std::threadthreads[5];for(inti=0;i<5;++i){threads[i]=std::thread(func1,200);}for(auto&th:threads){th.join();}return0;}

std::lock_gurad 相比於 std::unique_lock 更加輕量級,少了一些成員函數,std::unique_lock 類有 unlock 函數,可以手動釋放鎖,所以條件變量都配合 std::unique_lock 使用,而不是 std::lock_guard 。

因為條件變量在 wait 時需要有手動釋放鎖的能力,具體關於條件變量後面會講到。

4
std::atomic相關

c++11 提供了原子類型 std::atomic<T>,理論上這個 T 可以是任意類型,但是我平時只存放整形,別的還真的沒用過,整形有這種原子變量已經足夠方便,就不需要使用 std::mutex 來保護該變量啦。看一個計數器的代碼:

structOriginCounter{//普通的計數器intcount;std::mutexmutex_;voidadd(){std::lock_guard<std::mutex>lock(mutex_);++count;}voidsub(){std::lock_guard<std::mutex>lock(mutex_);--count;}intget(){std::lock_guard<std::mutex>lock(mutex_);returncount;}};structNewCounter{//使用原子變量的計數器std::atomic<int>count;voidadd(){++count;//count.store(++count);這種方式也可以}voidsub(){--count;//count.store(--count);}intget(){returncount.load();}}

是不是使用原子變量更加方便了呢?

5
std::call_once相關

c++11 提供了 std::call_once 來保證某一函數在多線程環境中只調用一次,它需要配合std::once_flag 使用,直接看使用代碼:

std::once_flagonceflag;voidCallOnce(){std::call_once(onceflag,[](){cout<<"callonce"<<endl;});}intmain(){std::threadthreads[5];for(inti=0;i<5;++i){threads[i]=std::thread(CallOnce);}for(auto&th:threads){th.join();}return0;}
6
std::volatile相關

貌似把 volatile 放在並發里介紹不太合適,但是貌似很多人都會把 volatile 和多線程聯繫在一起,那就一起介紹下吧。

volatile 通常用來建立內存屏障,volatile 修飾的變量,編譯器對訪問該變量的代碼通常不再進行優化,看下面代碼:

int*p=xxx;inta=*p;intb=*p;

a 和 b 都等於 p 指向的值,一般編譯器會對此做優化,把 *p 的值放入寄存器,就是傳說中的工作內存(不是主內存),之後 a 和 b 都等於寄存器的值。

但是如果中間 p 地址的值改變,內存上的值改變啦,但 a, b 還是從寄存器中取的值(不一定,看編譯器優化結果),這就不符合需求,所以在此對 p 加 volatile 修飾可以避免進行此類優化。


注意:volatile不能解決多線程安全問題,針對特種內存才需要使用volatile,它和atomic的特點如下:
•std::atomic用於多線程訪問的數據,且不用互斥量,用於並發編程中
•volatile用於讀寫操作不可以被優化掉的內存,用於特種內存中

7
std::condition_variable相關

條件變量是 c++11 引入的一種同步機制,它可以阻塞一個線程或者個線程,直到有線程通知或者超時才會喚醒正在阻塞的線程,條件變量需要和鎖配合使用,這裡的鎖就是上面介紹的std::unique_lock。

這裡使用條件變量實現一個 CountDownLatch :

classCountDownLatch{public:explicitCountDownLatch(uint32_tcount):count_(count);voidCountDown(){std::unique_lock<std::mutex>lock(mutex_);--count_;if(count_==0){cv_.notify_all();}}voidAwait(uint32_ttime_ms=0){std::unique_lock<std::mutex>lock(mutex_);while(count_>0){if(time_ms>0){cv_.wait_for(lock,std::chrono::milliseconds(time_ms));}else{cv_.wait(lock);}}}uint32_tGetCount()const{std::unique_lock<std::mutex>lock(mutex_);returncount_;}private:std::condition_variablecv_;mutablestd::mutexmutex_;uint32_tcount_=0;}

關於條件變量其實還涉及到通知丟失和虛假喚醒問題,因為不是本文的主題,這裡暫不介紹,大家有需要可以留言。

8
std::future相關

c++11關於異步操作提供了future相關的類,主要有 std::future、std::promise 和std::packaged_task ,std::future比std::thread 高級些,std::future 作為異步結果的傳輸通道,通過 get() 可以很方便的獲取線程函數的返回值。

std::promise 用來包裝一個值,將數據和 future 綁定起來,而 std::packaged_task 則用來包裝一個調用對象,將函數和 future 綁定起來,方便異步調用。而 std::future 是不可以複製的,如果需要複製放到容器中可以使用 std::shared_future 。

std::promise與std::future配合使用

#include<functional>#include<future>#include<iostream>#include<thread>usingnamespacestd;voidfunc(std::future<int>&fut){intx=fut.get();cout<<"value:"<<x<<endl;}intmain(){std::promise<int>prom;std::future<int>fut=prom.get_future();std::threadt(func,std::ref(fut));prom.set_value(144);t.join();return0;}

std::packaged_task 與 std::future 配合使用

#include<functional>#include<future>#include<iostream>#include<thread>usingnamespacestd;intfunc(intin){returnin+1;}intmain(){std::packaged_task<int(int)>task(func);std::future<int>fut=task.get_future();std::thread(std::move(task),5).detach();cout<<"result"<<fut.get()<<endl;return0;}

更多關於future的使用可以看我之前寫的關於線程池和定時器的文章。

三者之間的關係

std::future 用於訪問異步操作的結果,而 std::promise 和 std::packaged_task 在 future 高一層,它們內部都有一個 future,promise 包裝的是一個值,packaged_task 包裝的是一個函數。

當需要獲取線程中的某個值,可以使用std::promise,當需要獲取線程函數返回值,可以使用std::packaged_task。

9
async相關

async 是比 future,packaged_task,promise 更高級的東西,它是基於任務的異步操作,通過async 可以直接創建異步的任務,返回的結果會保存在 future 中。

不需要像 packaged_task 和 promise 那麼麻煩,關於線程操作應該優先使用 async,看一段使用代碼:

#include<functional>#include<future>#include<iostream>#include<thread>usingnamespacestd;intfunc(intin){returnin+1;}intmain(){autores=std::async(func,5);//res.wait();cout<<res.get()<<endl;//阻塞直到函數返回return0;}

使用 async異步執行函數是不是方便多啦。

async具體語法如下:

async(std::launch::async|std::launch::deferred,func,args...);

第一個參數是創建策略:

std::launch::async 表示任務執行在另一線程;

std::launch::deferred 表示延遲執行任務,調用 get 或者 wait 時才會執行,不會創建線程,惰性執行在當前線程。

如果不明確指定創建策略,以上兩個都不是 async 的默認策略,而是未定義,它是一個基於任務的程序設計,內部有一個調度器(線程池),會根據實際情況決定採用哪種策略。

若從 std::async 獲得的 std::future 未被移動或綁定到引用,則在完整表達式結尾, std::future的析構函數將阻塞直至異步計算完成,實際上相當於同步操作:

std::async(std::launch::async,[]{f();});//臨時量的析構函數等待f()std::async(std::launch::async,[]{g();});//f()完成前不開始</code></p></

注意:關於async啟動策略這裡網上和各種書籍介紹的五花八門,這裡會以cppreference為主。

•有時候我們如果想真正執行異步操作可以對async進行封裝,強制使用std::launch::async策略來調用async。


template<typenameF,typename...Args>inlineautoReallyAsync(F&&f,Args&&...params){returnstd::async(std::launch::async,std::forward<F>(f),std::forward<Args>(params)...);}

10
總結




•std::thread使線程的創建變得非常簡單,還可以獲取線程id等信息。

•std::mutex通過多種方式保證了線程安全,互斥量可以獨占,也可以重入,還可以設置互斥量的超時時間,避免一直阻塞等鎖。

•std::lock通過RAII技術方便了加鎖和解鎖調用,有std::lock_guard和std::unique_lock。

•std::atomic提供了原子變量,更方便實現實現保護,不需要使用互斥量

•std::call_once保證函數在多線程環境下只調用一次,可用於實現單例。

•volatile常用於讀寫操作不可以被優化掉的內存中。

•std::condition_variable提供等待的同步機制,可阻塞一個或多個線程,等待其它線程通知後喚醒。

•std::future用於異步調用的包裝和返回值。

•async更方便的實現了異步調用,異步調用優先使用async取代創建線程。

參考資料

https://blog.csdn.net/zhangzq86/article/details/70623394
https://zh.cppreference.com/w/cpp/atomic/atomic
https://zhuanlan.zhihu.com/p/33074506
https://www.runoob.com/w3cnote/c-volatile-keyword.html
https://zh.cppreference.com/w/cpp/thread/async
《深入應用c++11:代碼優化與工程級應用》
《Effective Modern C++》

推薦:

面試常問的 C/C++ 問題,你能答上來幾個?

C++ 面試必問:深入理解虛函數表

很多人搞不清 C++ 中的 delete 和 delete[ ] 的區別

看懂別人的代碼,總得懂點 C++ lambda 表達式吧

Java、C++ 內存模型都不知道,還敢說自己是高級工程師?

C++ std::thread 必須要熟悉的幾個知識點

現代 C++ 並發編程基礎

現代 C++ 智能指針使用入門

c++ thread join 和 detach 到底有什麼區別?

C++ 面試八股文:list、vector、deque 比較

C++經典面試題(最全,面中率最高)

C++ STL deque 容器底層實現原理(深度剖析)

STL vector push_back 和 emplace_back 區別

了解 C++ 多態與虛函數表

C++ 面試被問到的「左值引用和右值引用」
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()