libc++的 call once 为什么用 mutex/cv 而不是 atomic test_and_set - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
dangyuluo
V2EX    C++

libc++的 call once 为什么用 mutex/cv 而不是 atomic test_and_set

  •  
  •   dangyuluo 2023-04-11 16:14:47 +08:00 2125 次点击
    这是一个创建于 979 天前的主题,其中的信息可能已经有所发展或是发生改变。

    看了下源代码,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 ?

    12 条回复    2023-07-29 22:34:29 +08:00
    dangyuluo
        1
    dangyuluo  
    OP
       2023-04-11 16:15:20 +08:00
    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();
    }
    }
    ```
    dangyuluo
        2
    dangyuluo  
    OP
       2023-04-11 16:17:35 +08:00
    Abseil 的 call_once 就是采用了 compare_exchange_strong. 感觉更合理

    https://github.com/abseil/abseil-cpp/blob/master/absl/base/call_once.h#L174
    liberize
        3
    liberize  
       2023-04-11 19:09:13 +08:00 via Android
    假设 2 个线程同时执行 call_once ,必须保证 2 个线程都是函数执行完之后 call_once 才返回,你的这个例子显然不能保证,甚至可以执行多次。
    dangyuluo
        4
    dangyuluo  
    OP
       2023-04-12 00:58:20 +08:00
    @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).
    cnbatch
        5
    cnbatch  
       2023-04-12 02:24:02 +08:00
    我猜,可能是因为有潜在的“ABA 问题”,所以就索性用 mutex 简单粗暴免除隐患吧?
    dangyuluo
        6
    dangyuluo  
    OP
       2023-04-12 04:38:20 +08:00
    仔细读了一下文档,可能指的是这里:
    > The end of each active call synchronizes-with the next active call in that order.
    nlzy
        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 是真的丑陋。
    liberize
        8
    liberize  
       2023-04-12 08:26:48 +08:00 via Android
    @dangyuluo 这个说的是 was already called ,我说的是 was being called
    dangyuluo
        9
    dangyuluo  
    OP
       2023-04-12 13:20:29 +08:00
    @nlzy 请教了下一个在 C++委员会的同事,解释说是 call_once 需要保证第一个线程 throw 之后第二个线程可以继续执行。所以一个额外的同步是需要的。
    dangyuluo
        10
    dangyuluo  
    OP
       2023-04-12 14:38:58 +08:00
    @nlzy 忘了问一点了。。为什么所有的 call_once 要用同一个 mutex ,难道`call_once(func1)`和`call_once(func2)`要互相竞争么

    ```
    _LIBCPP_SAFE_STATIC static __libcpp_mutex_t mut = _LIBCPP_MUTEX_INITIALIZER;
    ```
    nlzy
        11
    nlzy  
       2023-04-12 15:34:14 +08:00
    @dangyuluo libc++ 的这个 static 令我瞬间觉得 Abseil 用 futex 实现的 spinlock 其实挺顺眼的。我收回“在我看来 libc++ 的代码是最合理的”那句话。
    j16ZgMV9cs6ZB23n
        12
    j16ZgMV9cs6ZB23n  
       2023-07-29 22:34:29 +08:00 via Android
    看了眼 想起最近自己魔改的 libc++吓了一跳,原来只有非 win32 call_once 才会遇到这个问题。

    在 win32 下,libc++会判断是否是 microsoft abi 然后使用系统函数 InitOnceExecuteOnce 交给系统实现。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3206 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 11:50 PVG 19:50 LAX 03:50 JFK 06:50
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86