我想知道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;
}
答案 0 :(得分:3)
您提议的实现不是线程安全的。它确保foo()
仅通过此代码调用一次,但不保证所有线程都会看到来自foo()
的调用的副作用。假设线程1执行比较并获得true,则在线程2调用foo()
之前,调度程序切换到线程2。线程2将变为false,跳过对foo()
的调用,然后继续。由于尚未执行对foo()
的调用,因此线程2可以在foo()
的任何副作用发生之前继续执行。
答案 1 :(得分:3)
已经呼叫过的快速路径可以等待。
gcc的实现看起来并不那么有效。我不知道为什么它的执行方式与使用非常量arg的static
局部变量的初始化相同,它使用的检查非常便宜(但不是免费的!),因为它已经是初始化。
http://en.cppreference.com/w/cpp/thread/call_once评论:
保证仅发生函数局部静态的初始化 甚至在从多个线程调用时,也可能更高效 比使用std :: call_once的等效代码。
为了实现高效实施,std::once_flag
可以有三种状态:
在大多数体系结构中检查带有获取负载的标志是非常便宜的(特别是x86,其中所有负载都是获取负载)。一旦设置为“完成”,它就不会对程序的其余部分进行修改,因此它可以在所有核心上保持在L1中缓存(除非您将其放在与经常修改的内容相同的缓存行中,从而创建错误共享)。
即使您的实现有效,它每次都会尝试使用原子CAS,这比负载获取要贵得多。
我还没有完全解码gcc正在为call_once
做什么,但在检查指针是否为NULL之前,它无条件地执行了一堆加载,并且有两个存储到线程本地存储。 (test rax,rax / je
)。但如果是,则调用std::__throw_system_error(int)
,因此它不是用于检测已经初始化的案例的保护变量。
所以它看起来无条件地调用__gthrw_pthread_once(int*, void (*)())
,并检查返回值。因此,您希望廉价地确保完成初始化,同时避免静态初始化惨败的用例非常糟糕。 (即你的构建过程控制静态对象的构造函数的顺序,而不是你在代码本身中放置的任何东西。)
因此,我建议使用static int dummy = init_function();
,其中虚拟是您实际想要构建的内容,或者只是为其副作用调用init_function
。 < / p>
然后在快速路径上,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,以及gcc的std::once_flag
实际代码。
您当然可以使用原子uint8_t 实现一个保护变量,该变量从初始化为非零,并且仅在调用完成时设置为零。在某些ISA上测试零可能稍微便宜一些,包括x86,如果编译器像gcc一样奇怪,并决定将其实际加载到寄存器中而不是使用cmp byte [guard], 0
。