5、std::call_once() (C++11)
std::call_once() 函数保证 f 只会被调用一次,即使在多线程环境也是如此。
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
比如下面的例子
#include <iostream>
#include <mutex>
#include <thread>
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();
}
只会有一句打印
Simple example: called once
首先是std::call_once() 函数的第一个参数 std::once_flag,在 Linux 平台下,其实就是封装了 pthread_once_t 数据结构。
/// mutex
struct once_flag
{
constexpr once_flag() noexcept = default;
/// Deleted copy constructor
once_flag(const once_flag&) = delete;
/// Deleted assignment operator
once_flag& operator=(const once_flag&) = delete;
private:
// For gthreads targets a pthread_once_t is used with pthread_once, but
// for most targets this doesn't work correctly for exceptional executions.
__gthread_once_t _M_once = __GTHREAD_ONCE_INIT;
struct _Prepare_execution;
template<typename _Callable, typename... _Args>
friend void
call_once(once_flag& __once, _Callable&& __f, _Args&&... __args);
};
std::once_flag::_Prepare_execution 使用 RAII 机制,设置 __once_callable 和 __once_call 两个变量。这两个变量是 TLS 变量,每个线程独享,某个线程修改不会影响到其他线程。
/// mutex
extern __thread void* __once_callable;
extern __thread void (*__once_call)();
// RAII type to set up state for pthread_once call.
struct once_flag::_Prepare_execution
{
template<typename _Callable>
explicit
_Prepare_execution(_Callable& __c)
{
// Store address in thread-local pointer:
__once_callable = std::__addressof(__c);
// Trampoline function to invoke the closure via thread-local pointer:
__once_call = [] { (*static_cast<_Callable*>(__once_callable))(); };
}
~_Prepare_execution()
{
// PR libstdc++/82481
__once_callable = nullptr;
__once_call = nullptr;
}
_Prepare_execution(const _Prepare_execution&) = delete;
_Prepare_execution& operator=(const _Prepare_execution&) = delete;
};
__once_callable 和 __once_call 的定义在 mutex.cc 这个文件里,另外 __once_proxy() 函数就是调用 __once_call 指向的函数。
/// mutex.cc
__thread void* __once_callable;
__thread void (*__once_call)();
extern "C" void __once_proxy()
{
// The caller stored a function pointer in __once_call. If it requires
// any state, it gets it from __once_callable.
__once_call();
}
std::call_once() 函数的定义就比较简单
- 首先将传入的 f 和 args 用 lambda 表达式封装成 callable
- 用 callable 初始化一个 once_flag::_Prepare_execution 对象
- 最后调用 pthread_once() 函数,执行 __once_proxy() 函数
从前面的分析,__once_proxy() 函数最终执行用户传入的 f 和 args。
/// mutex
template<typename _Callable, typename... _Args>
void
call_once(once_flag& __once, _Callable&& __f, _Args&&... __args)
{
// Closure type that runs the function
auto __callable = [&] {
std::__invoke(std::forward<_Callable>(__f),
std::forward<_Args>(__args)...);
};
once_flag::_Prepare_execution __exec(__callable);
// XXX pthread_once does not reset the flag if an exception is thrown.
if (int __e = __gthread_once(&__once._M_once, &__once_proxy))
__throw_system_error(__e);
}
std::call_once() 有一个 BUG,会一直阻塞调用线程。按照官方说法,如果抛出异常,后续调用可以继续执行。
If that invocation throws an exception, it is propagated to the caller of std::call_once, and flag is not flipped so that another call will be attempted (such a call to std::call_once is known as exceptional??).
使用 WSL(Debian) GCC12.2 测试,如果抛出异常,下一次执行将一直阻塞。比如下面也是官方的例子
#include <iostream>
#include <mutex>
#include <thread>
std::once_flag flag2;
void may_throw_function(bool do_throw) {
if (do_throw) {
// this may appear more than once
std::cout << "throw: call_once will retry\n";
throw std::exception();
}
// guaranteed once
std::cout << "Did not throw, call_once will not attempt again\n";
}
void do_once(bool do_throw) {
try {
std::call_once(flag2, 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();
}
只会打印一句,然后程序阻塞
throw: call_once will retry
原因出在 pthread_once() 函数上,pthread_once() 在抛出异常时,不会清理 flag 的值。
#include <pthread.h>
#include <stdio.h>
pthread_once_t flag_ = PTHREAD_ONCE_INIT; // 0
int call_count = 0;
extern "C" void func_() {
printf("Inside func_ call_count %d\n", call_count);
if (++call_count < 2)
throw 0; // exception
}
int main() {
printf("Before calling call_once flag_: %d\n", *(int*)&flag_);
try {
pthread_once(&flag_, func_);
} catch (...) {
printf("Inside catch all excepton flag_: %d\n", *(int*)&flag_);
// flag_ = PTHREAD_ONCE_INIT;
}
printf("before the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
pthread_once(&flag_, func_);
printf("after the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
}
阻塞在第二次调用 pthread_once() 函数上,输出为:
Before calling call_once flag_: 0
Inside func_ call_count 0
Inside catch all excepton flag_: 1
before the 2nd call to call_once flag_: 1
可以看到,第二次调用 pthread_once() 函数时,flag 的值为 1,而不是 0。另外,如果不抛出异常,flag 的值应该是多少呢?
#include <pthread.h>
#include <stdio.h>
pthread_once_t flag_ = PTHREAD_ONCE_INIT; // 0
int call_count = 0;
extern "C" void func_() {
printf("Inside func_ call_count %d\n", call_count);
// if (++call_count < 2)
// throw 0; // exception
}
int main() {
printf("Before calling call_once flag_: %d\n", *(int*)&flag_);
try {
pthread_once(&flag_, func_);
} catch (...) {
printf("Inside catch all excepton flag_: %d\n", *(int*)&flag_);
// flag_ = PTHREAD_ONCE_INIT;
}
printf("before the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
pthread_once(&flag_, func_);
printf("after the 2nd call to call_once flag_: %d\n", *(int*)&flag_);
}
可以看到,将抛出异常注释掉,flag 最后是 2。
Before calling call_once flag_: 0
Inside func_ call_count 0
before the 2nd call to call_once flag_: 2
after the 2nd call to call_once flag_: 2
所以,使用 std::call_once() 时,不能有异常抛出。