繁体   English   中英

std :: call_once锁是免费的吗?

[英]Is std::call_once lock free?

我想知道std :: call_once是否可以免费锁定。 使用互斥锁的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调用foo()之前切换到线程2。 线程2将变为false,跳过对foo()的调用,然后继续。 由于尚未执行对foo()的调用,因此线程2可以在发生foo()任何副作用之前继续执行。

已经被称为一次的快速路径可以等待

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

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

即使从多个线程调用,函数局部静态的初始化也只保证发生一次,并且可能比使用std :: call_once的等效代码更有效。


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

  • 执行完成:如果你发现这个状态,你已经完成了。
  • 正在执行:如果你发现这个:等到它改为完成(或者更改为失败的异常,在哪种情况下尝试声明它)
  • 执行未启动:如果发现此情况,请尝试将其CAS正在进行中并调用该函数。 如果CAS失败,则其他一些线程成功,因此转到等待完成状态。

在大多数体系结构中检查带有获取负载的标志是非常便宜的(特别是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(); 其中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

在Godbolt编译器资源管理器中查看它 ,以及gcc的std::once_flag实际代码。


您当然可以使用原子uint8_t自己实现一个保护变量,该原子uint8_t从初始化为非零,并且仅在调用完成时才设置为零。 在某些ISA上测试零可能会稍微便宜,包括x86,如果编译器像gcc一样奇怪,并决定实际将其加载到寄存器而不是使用cmp byte [guard], 0

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM