65
技術社區[雲棲]
C++11中once_flag,call_once實現分析
本文的分析基於llvm的libc++,而不是gun的libstdc++,因為libstdc++的代碼裏太多宏了,看起來蛋疼。
在多線程編程中,有一個常見的情景是某個任務隻需要執行一次。在C++11中提供了很方便的輔助類once_flag,call_once。
聲明
首先來看一下once_flag和call_once的聲明:
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是不允許修改的,拷貝構造函數和operator=函數都聲明為delete,這樣防止程序員亂用。
另外,call_once也是很簡單的,隻要傳進一個once_flag,回調函數,和參數列表就可以了。
示例
看一個示例:
https://en.cppreference.com/w/cpp/thread/call_once
#include <iostream> #include <thread> #include <mutex> std::once_flag flag; void do_once() { std::call_once(flag, [](){ std::cout << "Called once" << std::endl; }); } int main() { std::thread t1(do_once); std::thread t2(do_once); std::thread t3(do_once); std::thread t4(do_once); t1.join(); t2.join(); t3.join(); t4.join(); }保存為main.cpp,如果是用g++或者clang++來編繹:
g++ -std=c++11 -pthread main.cpp
clang++ -std=c++11 -pthread main.cpp
./a.out
可以看到,隻會輸出一行
Called once
值得注意的是,如果在函數執行中拋出了異常,那麼會有另一個在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(); }輸出的結果可能是0到3行throw,和一行once。
實際上once_flag相當於一個鎖,使用它的線程都會在上麵等待,隻有一個線程允許執行。如果該線程拋出異常,那麼從等待中的線程中選擇一個,重複上麵的流程。
實現分析
once_flag實際上隻有一個unsigned long __state_的成員變量,把call_once聲明為友元函數,這樣call_once能修改__state__變量:
struct once_flag { once_flag() _NOEXCEPT : __state_(0) {} private: once_flag(const once_flag&); // = delete; once_flag& operator=(const once_flag&); // = delete; unsigned long __state_; template<class _Callable> friend void call_once(once_flag&, _Callable); };call_once則用了一個__call_once_param類來包裝函數,很常見的模板編程技巧。
template <class _Fp> class __call_once_param { _Fp __f_; public: explicit __call_once_param(const _Fp& __f) : __f_(__f) {} void operator()() { __f_(); } }; template<class _Callable> void call_once(once_flag& __flag, _Callable __func) { if (__flag.__state_ != ~0ul) { __call_once_param<_Callable> __p(__func); __call_once(__flag.__state_, &__p, &__call_once_proxy<_Callable>); } }
最重要的是__call_once函數的實現:
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cv = PTHREAD_COND_INITIALIZER; void __call_once(volatile unsigned long& flag, void* arg, void(*func)(void*)) { pthread_mutex_lock(&mut); while (flag == 1) pthread_cond_wait(&cv, &mut); if (flag == 0) { #ifndef _LIBCPP_NO_EXCEPTIONS try { #endif // _LIBCPP_NO_EXCEPTIONS flag = 1; pthread_mutex_unlock(&mut); func(arg); pthread_mutex_lock(&mut); flag = ~0ul; pthread_mutex_unlock(&mut); pthread_cond_broadcast(&cv); #ifndef _LIBCPP_NO_EXCEPTIONS } catch (...) { pthread_mutex_lock(&mut); flag = 0ul; pthread_mutex_unlock(&mut); pthread_cond_broadcast(&cv); throw; } #endif // _LIBCPP_NO_EXCEPTIONS } else pthread_mutex_unlock(&mut); }裏麵用了全局的mutex和condition來做同步,還有異常處理的代碼。
其實當看到mutext和condition時,就明白是如何實現的了。裏麵有一係列的同步操作,可以參考另外一篇blog:
https://blog.csdn.net/hengyunabc/article/details/27969613 並行編程之條件變量(posix condition variables)
盡管代碼看起來很簡單,但是要仔細分析它的各種時序也比較複雜。
有個地方比較疑惑的:
對於同步的__state__變量,並沒有任何的memory order的保護,會不會有問題?
因為在JDK的代碼裏LockSupport和邏輯和上麵的__call_once函數類似,但是卻有memory order相關的代碼:
OrderAccess::fence();
其它的東東:
有個東東值得提一下,在C++中,static變量的初始化,並不是線程安全的。
比如
void func(){ static int value = 100; ... }
實際上相當於這樣的代碼:
i
nt __flag = 0 void func(){ static int value; if(!__flag){ value = 100; __flag = 1; } ... }
總結:
還有一件事情要考慮:所有的once_flag和call_once都共用全局的mutex和condition會不會有性能問題?
首先,像call_once這樣的需求在一個程序裏不會太多。另外,臨界區的代碼是比較很少的,隻有判斷各自的flag的代碼。
如果有上百上千個線程在等待once_flag,那麼pthread_cond_broadcast可能會造成“驚群”效果,但是如果有那麼多的線程都上等待,顯然程序設計有問題。
還有一個要注意的地方是once_flag的生命周期,它必須要比使用它的線程的生命周期要長。所以通常定義成全局變量比較好。
參考:
https://libcxx.llvm.org/
https://en.cppreference.com/w/cpp/thread/once_flag
https://en.cppreference.com/w/cpp/thread/call_once
最後更新:2017-04-03 07:57:13