您好,登錄后才能下訂單哦!
這篇文章主要介紹了C++中多線程std::call_once怎么用,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
在多線程的環境下,有些時候我們不需要某個函數被調用多次或者某些變量被初始化多次,它們僅僅只需要被調用一次或者初始化一次即可。很多時候我們為了初始化某些數據會寫出如下代碼,這些代碼在單線程中是沒有任何問題的,但是在多線程中就會出現不可預知的問題。
bool initialized = false; void foo() { if (!initialized) { do_initialize (); //1 initialized = true; } }
為了解決上述多線程中出現的資源競爭導致的數據不一致問題,我們大多數的處理方法就是使用互斥鎖來處理。只要上面①處進行保護,這樣共享數據對于并發訪問就是安全的。如下:
bool initialized = false; std::mutex resource_mutex; void foo() { std::unique_lock<std::mutex> lk(resource_mutex); // 所有線程在此序列化 if(!initialized) { do_initialize (); // 只有初始化過程需要保護 } initialized = true; lk.unlock(); // do other; }
但是,為了確保數據源已經初始化,每個線程都必須等待互斥量。為此,還有人想到使用“雙重檢查鎖模式”的辦法來提高效率,如下:
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 只有初始化過程需要保護 } initialized = true; } // do other; // 4 }
第一次讀取變量initialized
時不需要獲取鎖①,并且只有在initialized
為false時才需要獲取鎖。然后,當獲取鎖之后,會再檢查一次initialized
變量② (這就是雙重檢查的部分),避免另一線程在第一次檢查后再做初始化,并且讓當前線程獲取鎖。
但是上面這種情況也存在一定的風險,具體可以查閱著名的《C++和雙重檢查鎖定模式(DCLP)的風險》。
對此,C++標準委員會也認為條件競爭的處理很重要,所以C++標準庫提供了更好的處理方法:使用std::call_once函數來處理,其定義在頭文件#include<mutex>
中。std::call_once函數配合std::once_flag可以實現:多個線程同時調用某個函數,它可以保證多個線程對該函數只調用一次。它的定義如下:
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);
他接受的第一個參數類型為std::once_flag
,它只用默認構造函數構造,不能拷貝不能移動,表示函數的一種內在狀態。后面兩個參數很好理解,第一個傳入的是一個Callable。Callable簡單來說就是可調用的東西,大家熟悉的有函數、函數對象(重載了operator()
的類)、std::function
和函數指針,C++11新標準中還有std::bind
和lambda
(可以查看我的上一篇文章)。最后一個參數就是你要傳入的參數。 在使用的時候我們只需要定義一個non-local的std::once_flag
(非函數局部作用域內的),在調用時傳入參數即可,如下所示:
#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
保證函數func只被執行一次,如果有多個線程同時執行函數func調用,則只有一個活動線程(active call)會執行函數,其他的線程在這個線程執行返回之前會處于”passive execution”(被動執行狀態)——不會直接返回,直到活動線程對func調用結束才返回。對于所有調用函數func的并發線程,數據可見性都是同步的(一致的)。
但是,如果活動線程在執行func時拋出異常,則會從處于”passive execution”狀態的線程中挑一個線程成為活動線程繼續執行func,依此類推。一旦活動線程返回,所有”passive execution”狀態的線程也返回,不會成為活動線程。(實際上once_flag相當于一個鎖,使用它的線程都會在上面等待,只有一個線程允許執行。如果該線程拋出異常,那么從等待中的線程中選擇一個,重復上面的流程)。
std::call_once
在簽名設計時也很好地考慮到了參數傳遞的開銷問題,可以看到,不管是Callable還是Args
,都使用了&&
作為形參。他使用了一個template中的reference fold(我前面的文章也有介紹過),簡單分析:
如果傳入的是一個右值,那么Args
將會被推斷為Args
;
如果傳入的是一個const左值,那么Args
將會被推斷為const Args&
;
如果傳入的是一個non-const的左值,那么Args
將會被推斷為Args&
。
也就是說,不管你傳入的參數是什么,最終到達std::call_once
內部時,都會是參數的引用(右值引用或者左值引用),所以說是零拷貝的。那么還有一步呢,我們還得把參數傳到可調用對象里面執行我們要執行的函數,這一步同樣做到了零拷貝,這里用到了另一個標準庫的技術std::forward
(我前面的文章也有介紹過)。
如下,如果在函數執行中拋出了異常,那么會有另一個在once_flag
上等待的線程會執行。
#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,如果多線程的控制流同時到達 static 局部變量的聲明處,即使變量已在一個線程中初始化,其他線程并不知曉,仍會對其嘗試初始化。很多在不支持C++11標準的編譯器上,在實踐過程中,這樣的條件競爭是確實存在的,為此,C++11 規定,如果 static 局部變量正在初始化,線程到達此處時,將等待其完成,從而避免了 race condition,只有一個全局實例時,對于C++11,可以直接用 static 而不需要 std::call_once
,也就是說,在只需要一個全局實例情況下,可以成為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; }
感謝你能夠認真閱讀完這篇文章,希望小編分享的“C++中多線程std::call_once怎么用”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。