std::call_once 是免费的吗?

Is std::call_once lock free?

我想知道 std::call_once 锁是否空闲。 There 是 call_once 使用互斥锁的实现。但是我们为什么要使用互斥量呢?我尝试使用 atomic_bool 和 CAS 操作编写简单的实现。代码线程安全吗?

#include <iostream>
#include <thread>
#include <atomic>
#include <unistd.h>

using namespace std;
using my_once_flag = atomic<bool>;

void my_call_once(my_once_flag& flag, std::function<void()> foo) {
    bool expected = false;
    bool res = flag.compare_exchange_strong(expected, true,
                                            std::memory_order_release, std::memory_order_relaxed);
    if(res)
        foo();
}
my_once_flag flag;
void printOnce() {
    usleep(100);
    my_call_once(flag, [](){
       cout << "test" << endl;
    });
}

int main() {
    for(int i = 0; i< 500; ++i){
            thread([](){
                printOnce();
            }).detach();
    }
    return 0;
} 

您提出的实现不是线程安全的。它确实保证 foo() 只会通过这段代码被调用一次,但它不保证所有线程都会看到调用 foo() 的副作用。假设线程 1 执行了 compare 并得到 true,然后调度程序切换到线程 2,在线程 2 调用 foo() 之前。线程 2 将得到 false,跳过对 foo() 的调用,然后继续。由于尚未执行对 foo() 的调用,因此线程 2 可以在 foo() 的任何副作用发生之前继续执行。

already-called-once fast-path可以免等待.

gcc 的实现看起来效率不高。我不知道为什么它的实现方式与使用非常量 arg 的 static 局部变量的初始化方式不同,后者使用非常便宜(但不是免费!)的支票,用于已经存在的情况已初始化。

http://en.cppreference.com/w/cpp/thread/call_once 评论说:

Initialization of function-local statics is guaranteed to occur only once even when called from multiple threads, and may be more efficient than the equivalent code using std::call_once.


为了高效实施,std::once_flag 可以具有三种状态:

  • 执行完成:如果你找到这个状态,你就已经完成了。
  • 正在执行:如果你发现这个:等到它变为完成(或变为失败并有异常,在这种情况下尝试声明它)
  • 执行未开始:如果您发现此问题,请尝试将其 CAS 到正在进行中并调用该函数。如果 CAS 失败,其他线程成功,则进入等待完成状态。

在大多数架构上(尤其是所有负载都是获取负载的 x86),使用获取负载检查标志是非常便宜的。一旦设置为 "finished",程序的其余部分就不会对其进行修改,因此它可以在所有内核上缓存在 L1 中(除非您将它与经常修改的内容放在同一缓存行中,从而创建虚假共享).

即使您的实现有效,它每次都会尝试原子 CAS,这比加载获取贵得离谱。


我还没有完全理解 gcc 为 call_once 做了什么,但它无条件地执行了一堆加载,并在检查指针是否为 NULL 之前将两次存储到线程本地存储。 (test rax,rax / je)。但如果是,它会调用 std::__throw_system_error(int),因此它不是用来检测已初始化案例的保护变量。

所以看起来它无条件调用__gthrw_pthread_once(int*, void (*)()),并检查return值。因此,对于您希望以低廉的成本确保完成某些初始化同时避免静态初始化失败的用例来说,这非常糟糕。 (即,您的构建过程控制静态对象的构造函数的顺序,而不是您放入代码本身的任何内容。)

所以我建议使用 static int dummy = init_function();,其中 dummy 是您实际想要构造的东西,或者只是调用 init_function 的一种方式来获得其副作用。

然后在快速路径上,asm 来自:

int called_once();

void static_local(){
  static char dummy = called_once();
  (void)dummy;
}

看起来像这样:

static_local():
    movzx   eax, BYTE PTR guard variable for static_local()::dummy[rip]
    test    al, al
    je      .L18
    ret
 .L18:
    ... # code that implements basically what I described above: call or wait

See it on the Godbolt compiler explorer,以及 std::once_flag.

的 gcc 实际代码

你当然可以自己用一个原子 uint8_t 实现一个保护变量,它开始初始化为非零,并且只有在调用完成。如果编译器像 gcc 一样奇怪并决定实际将其加载到寄存器中而不是使用 cmp byte [guard], 0.

,则在某些 ISA 上测试零可能会稍微便宜一些,包括 x86