看了下源代码,call_once 的实现是
template<class _Callable, class... _Args>
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 交给系统实现。 |