2016-11-20 30 views
3

我想知道std :: call_once鎖是否可用。 There是使用互斥鎖的call_once實現。但爲什麼我們應該使用互斥鎖?我試圖用atomic_bool和CAS操作編寫簡單的實現。代碼線程安全嗎?是否爲std :: call_once lock free?

#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; 
} 
+4

這是一個關於'std :: call_once'的問題和一個關於查看替換實現的問題。請選擇一個問題。 –

+2

你的實現不是線程安全的,因爲如果'foo()'在執行中,'call_once()'需要等待它完成。沒有無鎖的方式來做到這一點。 –

+0

我想了解std :: call_once是否鎖定空閒以及它是否可以實現鎖定空閒。 – Fixturessd

回答

3

您建議的實現不是線程安全的。它確實保證foo()只會通過此代碼被調用一次,但並不能保證所有線程都會看到foo()調用的副作用。假設線程1執行比較並且爲真,那麼在線程2調用foo()之前,調度器切換到線程2。線程2將變爲false,請跳至foo(),然後繼續。由於foo()的調用尚未執行,因此線程2可以在發生foo()的任何副作用之前繼續執行。

+0

感謝您的解釋! – Fixturessd

3

已經叫做一次的快速路徑可以等待免費

gcc的實現看起來並不是那麼高效。我不知道爲什麼它沒有以與初始化static局部變量一樣的方式實現,它帶有一個非常量arg,它使用了一個非常便宜(但不是免費的)檢查,因爲它已經被初始化了。

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();其中啞或者是你真正想要建造,或只是一些一個方法呼籲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,開始了初始化爲非零,並設置爲零通話結束時才。在一些ISA上測試零可能會稍微便宜些,包括x86,如果編譯器像gcc一樣奇怪,並決定實際將其加載到寄存器中而不是使用cmp byte [guard], 0

+0

有一個call_once的實現,它使用線程本地存儲來避免重新獲取; gcc可能會使用它。 (當然,因爲x86上的獲取是免費的,所以我不知道它爲什麼會......) – tony

+0

@tony:謝謝,這聽起來像是一個合理的理論。 GNU庫不專門用於x86,如果它只是一個通用的C++實現,我不會感到驚訝。也許像編譯器那樣使用僅僅使用可移植的C++初始化一個'static'而不真正初始化一個'static'來獲得asm並不容易。 (我沒有關注call_once的所有文檔,但是如果IDK的異常語義與'static'的初始化符相匹配,則IDK不會)。cppreference.com會警告說'static'局部變量可能更高效,所以編譯器做這樣的事情。 –

+0

靜態的使用不能解決一次調用的函數是特定類或結構實例的延遲初始化程序的用例。示例:struct A {void get(){...} void initOnce(){...} // other stuff};在這裏,我希望第一次在類A的實例上調用get()時,會爲該實例運行initOnce。 – Fabio

相關問題