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 · 1715 次点击
    这是一个创建于 623 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    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   ·   实用小工具   ·   3131 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:41 · PVG 20:41 · LAX 04:41 · JFK 07:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.