c++11關于并發(fā)引入了好多好東西,這里按照如下順序介紹:
std::thread相關
std::mutex相關
std::lock相關
std::atomic相關
std::call_once相關
volatile相關
std::condition_variable相關
std::future相關
async相關
std::thread相關
c++11之前你可能使用pthread_xxx來創(chuàng)建線程,繁瑣且不易讀,c++11引入了std::thread來創(chuàng)建線程,支持對線程join或者detach。直接看代碼:
using namespace std;
int main() {
auto func = []() {
for (int i = 0; i < 10; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread t(func);
if (t.joinable()) {
t.detach();
}
auto func1 = [](int k) {
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread tt(func1, 20);
if (tt.joinable()) { // 檢查線程可否被join
tt.join();
}
return 0;
}
上述代碼中,函數(shù)func和func1運行在線程對象t和tt中,從剛創(chuàng)建對象開始就會新建一個線程用于執(zhí)行函數(shù),調用join函數(shù)將會阻塞主線程,直到線程函數(shù)執(zhí)行結束,線程函數(shù)的返回值將會被忽略。如果不希望線程被阻塞執(zhí)行,可以調用線程對象的detach函數(shù),表示將線程和線程對象分離。
如果沒有調用join或者detach函數(shù),假如線程函數(shù)執(zhí)行時間較長,此時線程對象的生命周期結束調用析構函數(shù)清理資源,這時可能會發(fā)生錯誤,這里有兩種解決辦法,一個是調用join(),保證線程函數(shù)的生命周期和線程對象的生命周期相同,另一個是調用detach(),將線程和線程對象分離,這里需要注意,如果線程已經和對象分離,那我們就再也無法控制線程什么時候結束了,不能再通過join來等待線程執(zhí)行完。
這里可以對thread進行封裝,避免沒有調用join或者detach可導致程序出錯的情況出現(xiàn):
class ThreadGuard {
public:
enum class DesAction { join, detach };
ThreadGuard(std::thread&& t, DesAction a) : 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() { return t_; }
private:
std::thread t_;
DesAction action_;
};
int main() {
ThreadGuard t(std::thread([]() {
for (int i = 0; i < 10; ++i) {
std::cout << "thread guard " << i << " ";
}
std::cout << std::endl;}), ThreadGuard::DesAction::join);
return 0;
}
c++11還提供了獲取線程id,或者系統(tǒng)cpu個數(shù),獲取thread native_handle,使得線程休眠等功能
std::thread t(func);
cout << "當前線程ID " << t.get_id() << endl;
cout << "當前cpu個數(shù) " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相關操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::mutex相關
std::mutex是一種線程同步的手段,用于保存多線程同時操作的共享數(shù)據(jù)。
mutex分為四種:
std::mutex:獨占的互斥量,不能遞歸使用,不帶超時功能
std::recursive_mutex:遞歸互斥量,可重入,不帶超時功能
std::timed_mutex:帶超時的互斥量,不能遞歸
std::recursive_timed_mutex:帶超時的互斥量,可以遞歸使用
拿一個std::mutex和std::timed_mutex舉例吧,別的都是類似的使用方式:
std::mutex:
using namespace std;
std::mutex mutex_;
int main() {
auto func1 = [](int k) {
mutex_.lock();
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::timed_mutex:
using namespace std;
std::timed_mutex timed_mutex_;
int main() {
auto func1 = [](int k) {
timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
timed_mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::lock相關
這里主要介紹兩種RAII方式的鎖封裝,可以動態(tài)的釋放鎖資源,防止線程由于編碼失誤導致一直持有鎖。
c++11主要有std::lock_guard和std::unique_lock兩種方式,使用方式都類似,如下:
using namespace std;
std::mutex mutex_;
int main() {
auto func1 = [](int k) {
// std::lock_guard<std::mutex> lock(mutex_);
std::unique_lock<std::mutex> lock(mutex_);
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::lock_gurad相比于std::unique_lock更加輕量級,少了一些成員函數(shù),std::unique_lock類有unlock函數(shù),可以手動釋放鎖,所以條件變量都配合std::unique_lock使用,而不是std::lock_guard,因為條件變量在wait時需要有手動釋放鎖的能力,具體關于條件變量后面會講到。
std::atomic相關
c++11提供了原子類型std::atomic<T>,理論上這個T可以是任意類型,但是我平時只存放整形,別的還真的沒用過,整形有這種原子變量已經足夠方便,就不需要使用std::mutex來保護該變量啦??匆粋€計數(shù)器的代碼:
struct OriginCounter { // 普通的計數(shù)器
int count;
std::mutex mutex_;
void add() {
std::lock_guard<std::mutex> lock(mutex_);
++count;
}
void sub() {
std::lock_guard<std::mutex> lock(mutex_);
--count;
}
int get() {
std::lock_guard<std::mutex> lock(mutex_);
return count;
}
};
struct NewCounter { // 使用原子變量的計數(shù)器
std::atomic<int> count;
void add() {
++count;
// count.store(++count);這種方式也可以
}
void sub() {
--count;
// count.store(--count);
}
int get() {
return count.load();
}
};
是不是使用原子變量更加方便了呢?
std::call_once相關
c++11提供了std::call_once來保證某一函數(shù)在多線程環(huán)境中只調用一次,它需要配合std::once_flag使用,直接看使用代碼吧:
std::once_flag onceflag;
void CallOnce() {
std::call_once(onceflag, []() {
cout << "call once" << endl;
});
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(CallOnce);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
volatile相關
貌似把volatile放在并發(fā)里介紹不太合適,但是貌似很多人都會把volatile和多線程聯(lián)系在一起,那就一起介紹下吧。
volatile通常用來建立內存屏障,volatile修飾的變量,編譯器對訪問該變量的代碼通常不再進行優(yōu)化,看下面代碼:
int *p = xxx;
int a = *p;
int b = *p;
a和b都等于p指向的值,一般編譯器會對此做優(yōu)化,把*p的值放入寄存器,就是傳說中的工作內存(不是主內存),之后a和b都等于寄存器的值,但是如果中間p地址的值改變,內存上的值改變啦,但a,b還是從寄存器中取的值(不一定,看編譯器優(yōu)化結果),這就不符合需求,所以在此對p加volatile修飾可以避免進行此類優(yōu)化。
std::condition_variable相關
條件變量是c++11引入的一種同步機制,它可以阻塞一個線程或者個線程,直到有線程通知或者超時才會喚醒正在阻塞的線程,條件變量需要和鎖配合使用,這里的鎖就是上面介紹的std::unique_lock。
這里使用條件變量實現(xiàn)一個CountDownLatch:
class CountDownLatch {
public:
explicit CountDownLatch(uint32_t count) : count_(count);
void CountDown() {
std::unique_lock<std::mutex> lock(mutex_);
--count_;
if (count_ == 0) {
cv_.notify_all();
}
}
void Await(uint32_t time_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_t GetCount() const {
std::unique_lock<std::mutex> lock(mutex_);
return count_;
}
private:
std::condition_variable cv_;
mutable std::mutex mutex_;
uint32_t count_ = 0;
};
關于條件變量其實還涉及到通知丟失和虛假喚醒問題,因為不是本文的主題,這里暫不介紹,大家有需要可以留言。
std::future相關
c++11關于異步操作提供了future相關的類,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高級些,std::future作為異步結果的傳輸通道,通過get()可以很方便的獲取線程函數(shù)的返回值,std::promise用來包裝一個值,將數(shù)據(jù)和future綁定起來,而std::packaged_task則用來包裝一個調用對象,將函數(shù)和future綁定起來,方便異步調用。而std::future是不可以復制的,如果需要復制放到容器中可以使用std::shared_future。
std::promise與std::future配合使用
using namespace std;
void func(std::future<int>& fut) {
int x = fut.get();
cout << "value: " << x << endl;
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(func, std::ref(fut));
prom.set_value(144);
t.join();
return 0;
}
std::packaged_task與std::future配合使用
using namespace std;
int func(int in) {
return in + 1;
}
int main() {
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;
return 0;
}
更多關于future的使用可以看我之前寫的關于線程池和定時器的文章。
三者之間的關系
std::future用于訪問異步操作的結果,而std::promise和std::packaged_task在future高一層,它們內部都有一個future,promise包裝的是一個值,packaged_task包裝的是一個函數(shù),當需要獲取線程中的某個值,可以使用std::promise,當需要獲取線程函數(shù)返回值,可以使用std::packaged_task。
async相關
async是比future,packaged_task,promise更高級的東西,它是基于任務的異步操作,通過async可以直接創(chuàng)建異步的任務,返回的結果會保存在future中,不需要像packaged_task和promise那么麻煩,關于線程操作應該優(yōu)先使用async,看一段使用代碼:
using namespace std;
int func(int in) { return in + 1; }
int main() {
auto res = std::async(func, 5);
// res.wait();
cout << res.get() << endl; // 阻塞直到函數(shù)返回
return 0;
}
使用async異步執(zhí)行函數(shù)是不是方便多啦。
async具體語法如下:
async(std::launch::async | std::launch::deferred, func, args...);
第一個參數(shù)是創(chuàng)建策略:
std::launch::async表示任務執(zhí)行在另一線程
std::launch::deferred表示延遲執(zhí)行任務,調用get或者wait時才會執(zhí)行,不會創(chuàng)建線程,惰性執(zhí)行在當前線程。
如果不明確指定創(chuàng)建策略,以上兩個都不是async的默認策略,而是未定義,它是一個基于任務的程序設計,內部有一個調度器(線程池),會根據(jù)實際情況決定采用哪種策略。
若從 std::async 獲得的 std::future 未被移動或綁定到引用,則在完整表達式結尾, std::future的析構函數(shù)將阻塞直至異步計算完成,實際上相當于同步操作:
std::async(std::launch::async, []{ f(); }); // 臨時量的析構函數(shù)等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不開始
注意:關于async啟動策略這里網上和各種書籍介紹的五花八門,這里會以cppreference為主。
? 有時候我們如果想真正執(zhí)行異步操作可以對async進行封裝,強制使用std::launch::async策略來調用async。
template <typename F, typename... Args>
inline auto ReallyAsync(F&& f, Args&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
}
關于c++11關于并發(fā)的新特性就介紹到這里
參考資料
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:代碼優(yōu)化與工程級應用》
《Effective Modern C++》
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!