C++多線程std::call_once的使用
在多線程的環(huán)境下,有些時(shí)候我們不需要某個(gè)函數(shù)被調(diào)用多次或者某些變量被初始化多次,它們僅僅只需要被調(diào)用一次或者初始化一次即可。很多時(shí)候我們?yōu)榱顺跏蓟承?shù)據(jù)會寫出如下代碼,這些代碼在單線程中是沒有任何問題的,但是在多線程中就會出現(xiàn)不可預(yù)知的問題。
bool initialized = false;
void foo() {
if (!initialized) {
do_initialize (); //1
initialized = true;
}
}為了解決上述多線程中出現(xiàn)的資源競爭導(dǎo)致的數(shù)據(jù)不一致問題,我們大多數(shù)的處理方法就是使用互斥鎖來處理。只要上面①處進(jìn)行保護(hù),這樣共享數(shù)據(jù)對于并發(fā)訪問就是安全的。如下:
bool initialized = false;
std::mutex resource_mutex;
void foo() {
std::unique_lock<std::mutex> lk(resource_mutex); // 所有線程在此序列化
if(!initialized) {
do_initialize (); // 只有初始化過程需要保護(hù)
}
initialized = true;
lk.unlock();
// do other;
}但是,為了確保數(shù)據(jù)源已經(jīng)初始化,每個(gè)線程都必須等待互斥量。為此,還有人想到使用“雙重檢查鎖模式”的辦法來提高效率,如下:
bool initialized = false;
std::mutex resource_mutex;
void foo() {
if(!initialized) { // 1
std::unique_lock<std::mutex> lk(resource_mutex); // 2 所有線程在此序列化
if(!initialized) {
do_initialize (); // 3 只有初始化過程需要保護(hù)
}
initialized = true;
}
// do other; // 4
}第一次讀取變量initialized時(shí)不需要獲取鎖①,并且只有在initialized為false時(shí)才需要獲取鎖。然后,當(dāng)獲取鎖之后,會再檢查一次initialized變量② (這就是雙重檢查的部分),避免另一線程在第一次檢查后再做初始化,并且讓當(dāng)前線程獲取鎖。
但是上面這種情況也存在一定的風(fēng)險(xiǎn),具體可以查閱著名的《C++和雙重檢查鎖定模式(DCLP)的風(fēng)險(xiǎn)》。
對此,C++標(biāo)準(zhǔn)委員會也認(rèn)為條件競爭的處理很重要,所以C++標(biāo)準(zhǔn)庫提供了更好的處理方法:使用std::call_once函數(shù)來處理,其定義在頭文件#include<mutex>中。std::call_once函數(shù)配合std::once_flag可以實(shí)現(xiàn):多個(gè)線程同時(shí)調(diào)用某個(gè)函數(shù),它可以保證多個(gè)線程對該函數(shù)只調(diào)用一次。它的定義如下:
struct once_flag
{
constexpr once_flag() noexcept;
once_flag(const once_flag&) = delete;
once_flag& operator=(const once_flag&) = delete;
};
template<class Callable, class ...Args>
void call_once(once_flag& flag, Callable&& func, Args&&... args);他接受的第一個(gè)參數(shù)類型為std::once_flag,它只用默認(rèn)構(gòu)造函數(shù)構(gòu)造,不能拷貝不能移動,表示函數(shù)的一種內(nèi)在狀態(tài)。后面兩個(gè)參數(shù)很好理解,第一個(gè)傳入的是一個(gè)Callable。Callable簡單來說就是可調(diào)用的東西,大家熟悉的有函數(shù)、函數(shù)對象(重載了operator()的類)、std::function和函數(shù)指針,C++11新標(biāo)準(zhǔn)中還有std::bind和lambda(可以查看我的上一篇文章)。最后一個(gè)參數(shù)就是你要傳入的參數(shù)。 在使用的時(shí)候我們只需要定義一個(gè)non-local的std::once_flag(非函數(shù)局部作用域內(nèi)的),在調(diào)用時(shí)傳入?yún)?shù)即可,如下所示:
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag1;
void simple_do_once() {
std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}
int main() {
std::thread st1(simple_do_once);
std::thread st2(simple_do_once);
std::thread st3(simple_do_once);
std::thread st4(simple_do_once);
st1.join();
st2.join();
st3.join();
st4.join();
}call_once保證函數(shù)func只被執(zhí)行一次,如果有多個(gè)線程同時(shí)執(zhí)行函數(shù)func調(diào)用,則只有一個(gè)活動線程(active call)會執(zhí)行函數(shù),其他的線程在這個(gè)線程執(zhí)行返回之前會處于”passive execution”(被動執(zhí)行狀態(tài))——不會直接返回,直到活動線程對func調(diào)用結(jié)束才返回。對于所有調(diào)用函數(shù)func的并發(fā)線程,數(shù)據(jù)可見性都是同步的(一致的)。
但是,如果活動線程在執(zhí)行func時(shí)拋出異常,則會從處于”passive execution”狀態(tài)的線程中挑一個(gè)線程成為活動線程繼續(xù)執(zhí)行func,依此類推。一旦活動線程返回,所有”passive execution”狀態(tài)的線程也返回,不會成為活動線程。(實(shí)際上once_flag相當(dāng)于一個(gè)鎖,使用它的線程都會在上面等待,只有一個(gè)線程允許執(zhí)行。如果該線程拋出異常,那么從等待中的線程中選擇一個(gè),重復(fù)上面的流程)。
std::call_once在簽名設(shè)計(jì)時(shí)也很好地考慮到了參數(shù)傳遞的開銷問題,可以看到,不管是Callable還是Args,都使用了&&作為形參。他使用了一個(gè)template中的reference fold(我前面的文章也有介紹過),簡單分析:
- 如果傳入的是一個(gè)右值,那么
Args將會被推斷為Args; - 如果傳入的是一個(gè)const左值,那么
Args將會被推斷為const Args&; - 如果傳入的是一個(gè)non-const的左值,那么
Args將會被推斷為Args&。
也就是說,不管你傳入的參數(shù)是什么,最終到達(dá)std::call_once內(nèi)部時(shí),都會是參數(shù)的引用(右值引用或者左值引用),所以說是零拷貝的。那么還有一步呢,我們還得把參數(shù)傳到可調(diào)用對象里面執(zhí)行我們要執(zhí)行的函數(shù),這一步同樣做到了零拷貝,這里用到了另一個(gè)標(biāo)準(zhǔn)庫的技術(shù)std::forward(我前面的文章也有介紹過)。
如下,如果在函數(shù)執(zhí)行中拋出了異常,那么會有另一個(gè)在once_flag上等待的線程會執(zhí)行。
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
inline void may_throw_function(bool do_throw) {
// only one instance of this function can be run simultaneously
if (do_throw) {
std::cout << "throw\n"; // this message may be printed from 0 to 3 times
// if function exits via exception, another function selected
throw std::exception();
}
std::cout << "once\n"; // printed exactly once, it's guaranteed that
// there are no messages after it
}
inline void do_once(bool do_throw) {
try {
std::call_once(flag, may_throw_function, do_throw);
} catch (...) {
}
}
int main() {
std::thread t1(do_once, true);
std::thread t2(do_once, true);
std::thread t3(do_once, false);
std::thread t4(do_once, true);
t1.join();
t2.join();
t3.join();
t4.join();
}std::call_once 也可以用在類中:
#include <iostream>
#include <mutex>
#include <thread>
class A {
public:
void f() {
std::call_once(flag_, &A::print, this);
std::cout << 2;
}
private:
void print() { std::cout << 1; }
private:
std::once_flag flag_;
};
int main() {
A a;
std::thread t1{&A::f, &a};
std::thread t2{&A::f, &a};
t1.join();
t2.join();
} // 122還有一種初始化過程中潛存著條件競爭:static 局部變量在聲明后就完成了初始化,這存在潛在的 race condition,如果多線程的控制流同時(shí)到達(dá) static 局部變量的聲明處,即使變量已在一個(gè)線程中初始化,其他線程并不知曉,仍會對其嘗試初始化。很多在不支持C++11標(biāo)準(zhǔn)的編譯器上,在實(shí)踐過程中,這樣的條件競爭是確實(shí)存在的,為此,C++11 規(guī)定,如果 static 局部變量正在初始化,線程到達(dá)此處時(shí),將等待其完成,從而避免了 race condition,只有一個(gè)全局實(shí)例時(shí),對于C++11,可以直接用 static 而不需要 std::call_once,也就是說,在只需要一個(gè)全局實(shí)例情況下,可以成為std::call_once的替代方案,典型的就是單例模式了:
template <typename T>
class Singleton {
public:
static T& Instance();
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
template <typename T>
T& Singleton<T>::Instance() {
static T instance;
return instance;
}今天的內(nèi)容就到這里了。
參考:
std::call_once - C++中文 - API參考文檔 (apiref.com)
到此這篇關(guān)于C++多線程std::call_once的使用的文章就介紹到這了,更多相關(guān)C++ std::call_once內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C/C++實(shí)現(xiàn)獲取系統(tǒng)時(shí)間的示例代碼
C 標(biāo)準(zhǔn)庫提供了 time() 函數(shù)與 localtime() 函數(shù)可以獲取到當(dāng)前系統(tǒng)的日歷時(shí)間。本文將通過一些簡單的示例為大家講講C++獲取系統(tǒng)時(shí)間的具體方法,需要的可以參考一下2022-12-12
一文學(xué)會數(shù)據(jù)結(jié)構(gòu)-堆
本文主要介紹了數(shù)據(jù)結(jié)構(gòu)-堆,文中通過圖片和大量的代碼講解的非常詳細(xì),需要學(xué)習(xí)的朋友可以參考下這篇文章,希望可以幫助到你2021-08-08
C++之CNoTrackObject類和new delete操作符的重載實(shí)例
這篇文章主要介紹了C++之CNoTrackObject類和new delete操作符的重載實(shí)例,是C++程序設(shè)計(jì)中比較重要的概念,需要的朋友可以參考下2014-10-10
C語言模擬實(shí)現(xiàn)C++的繼承與多態(tài)示例
本篇文章主要介紹了C語言模擬實(shí)現(xiàn)C++的繼承與多態(tài)示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05
C++實(shí)現(xiàn)冒泡排序(BubbleSort)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)冒泡排序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04

