簡體   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