
看了下源代码,call_once 的实现是
template<class _Callable, class... _Argsgt; inline _LIBCPP_INLINE_VISIBILITY void call_once(once_flag& __flag, _Callable&& __func, _Args&&... __args) { if (__libcpp_acquire_load(&__flag.__state_) != ~once_flag::_State_type(0)) { typedef tuple<_Callable&&, _Args&&...> _Gp; _Gp __f(_VSTD::forward<_Callable>(__func), _VSTD::forward<_Args>(__args)...); __call_once_param<_Gp> __p(__f); __call_once(__flag.__state_, &__p, &__call_once_proxy<_Gp>); } } 其中__call_once简化后:
void __call_once(volatile once_flag::_State_type& flag, void* arg, void (*func)(void*)) { __libcpp_mutex_lock(&mut); while (flag == 1) __libcpp_condvar_wait(&cv, &mut); if (flag == 0) { try { __libcpp_relaxed_store(&flag, once_flag::_State_type(1)); __libcpp_mutex_unlock(&mut); func(arg); __libcpp_mutex_lock(&mut); __libcpp_atomic_store(&flag, ~once_flag::_State_type(0), _AO_Release); __libcpp_mutex_unlock(&mut); __libcpp_condvar_broadcast(&cv); } catch (...) { __libcpp_mutex_lock(&mut); __libcpp_relaxed_store(&flag, once_flag::_State_type(0)); __libcpp_mutex_unlock(&mut); __libcpp_condvar_broadcast(&cv); throw; } } else __libcpp_mutex_unlock(&mut); } 请问这里是出于什么考量不使用 atomic test_and_set ?
1 dangyuluo OP chatgpt 给的样例: ```cpp template<typename Callable, typename ...Args> void call_once(std::once_flag& flag, Callable&& func, Args&&... args) { // Atomically check if the flag is set if (!flag.test_and_set()) { // The flag is not set, so call the function std::forward<Callable>(func)(std::forward<Args>(args)...); // Reset the flag to indicate that the function has been called flag.clear(); } } ``` |
2 dangyuluo OP Abseil 的 call_once 就是采用了 compare_exchange_strong. 感觉更合理 https://github.com/abseil/abseil-cpp/blob/master/absl/base/call_once.h#L174 |
3 liberize 2023-04-11 19:09:13 +08:00 via Android 假设 2 个线程同时执行 call_once ,必须保证 2 个线程都是函数执行完之后 call_once 才返回,你的这个例子显然不能保证,甚至可以执行多次。 |
4 dangyuluo OP @liberize 可是第二个线程的 call_once 并不会是 blocking 的吧,cppreference 上是这么解释的: > If, by the time call_once is called, flag indicates that f was already called, call_once returns right away (such a call to call_once is known as passive). |
5 cnbatch 2023-04-12 02:24:02 +08:00 我猜,可能是因为有潜在的“ABA 问题”,所以就索性用 mutex 简单粗暴免除隐患吧? |
6 dangyuluo OP 仔细读了一下文档,可能指的是这里: > The end of each active call synchronizes-with the next active call in that order. |
7 nlzy 2023-04-12 08:04:16 +08:00 ChatGPT 的实现已经完全错了。合理的 call_once 应当会等待其他线程并阻塞的,只要没看到阻塞的代码就肯定是错的。 Abseil 没有保证异常下的语义,所以不能用来代替 C++ 标准里的 std::call_once 。 只有 libc++ 实现了全部的 std::call_once 的语义。 在我看来 libc++ 的代码是最合理的,call_once 里的第一行 acquire_load 已经是一个 fast path 优化了,如果这个 fast path 进不去,没有理由再去利用其他的机制(包括 test_and_set 或者 compare_and_swap )增加一个 fast path 优化。而且 call_once 是绝对不可能用无锁算法实现的,因为 call_once 会等待其他线程,那在用户态等待其他线程不用 mtx/cv 那还能用啥?在我看来 Abseil 自己包装一个 spinlock 是真的丑陋。 |
8 liberize 2023-04-12 08:26:48 +08:00 via Android @dangyuluo 这个说的是 was already called ,我说的是 was being called |
9 dangyuluo OP @nlzy 请教了下一个在 C++委员会的同事,解释说是 call_once 需要保证第一个线程 throw 之后第二个线程可以继续执行。所以一个额外的同步是需要的。 |
10 dangyuluo OP @nlzy 忘了问一点了。。为什么所有的 call_once 要用同一个 mutex ,难道`call_once(func1)`和`call_once(func2)`要互相竞争么 ``` _LIBCPP_SAFE_STATIC static __libcpp_mutex_t mut = _LIBCPP_MUTEX_INITIALIZER; ``` |
11 nlzy 2023-04-12 15:34:14 +08:00 @dangyuluo libc++ 的这个 static 令我瞬间觉得 Abseil 用 futex 实现的 spinlock 其实挺顺眼的。我收回“在我看来 libc++ 的代码是最合理的”那句话。 |
12 j16ZgMV9cs6ZB23n 2023-07-29 22:34:29 +08:00 via Android 看了眼 想起最近自己魔改的 libc++吓了一跳,原来只有非 win32 call_once 才会遇到这个问题。 在 win32 下,libc++会判断是否是 microsoft abi 然后使用系统函数 InitOnceExecuteOnce 交给系统实现。 |