2015-09-04 44 views
9
int f(int); 

多個線程可以調用此函數。 該功能應返回如何使這個功能線程安全和快速?

argument * argument_used_in_first_call_to_function 

我已編碼如下。儘管它是線程安全的,但它並不快,因爲它使用互斥鎖/解鎖。在線程安全的同時是否有更快的解決方案?

mutex mut1; 
int f(int x) 
{ 
    pthread_mutex_lock(mut1); 
    static bool first_init = true; 
    static int first_arg = 0; 

    if (first_init) 
    { 
    first_arg = x; 
    first_init = false; 
    } 
    pthread_mutex_unlock(mut1); 
    return x * first_arg; 
} 
+4

看看pthread_once。另請參閱C11/C++ 11原子,以獲得更快的標誌防護。 – Jeff

+0

請縮進您的代碼。 –

+1

C或C++?他們有不同的實現。 – edmz

回答

6

如果你的編譯器支持它們,Mike的神奇靜態回答是最好的。如果您使用Visual Studio 2013,最好的方法是使用std::call_once,而不是自定義標誌和互斥。

#include <mutex> 
namespace { 
    std::once_flag fFirstCallFlag; 
} 
int f(int arg) { 
    static int firstValue; 
    std::call_once(fFirstCallFlag, [&firstValue, arg] { firstValue = arg; }); 
    return firstValue * arg; 
} 
+0

你確定,'firstValue'可以是一個本地靜態?我想,如果你沒有明確地初始化它們,它們會在第一次被隱含地初始化爲零,控制流經過它們。 – MikeMB

+1

不,這種隱式的零初始化是靜態的,不是動態的,所以它發生在程序加載時。如果它是一個帶有構造函數的對象,或者初始值需要動態計算,那麼你就是對的。 –

+0

對,我忘了那個(就像我對call_once所做的那樣)。謝謝(你的)信息。 – MikeMB

12

如果你有一個C++ 11兼容的編譯器
(例如,不VS2013)

兩個,最簡單,最有效的方法是隻寫:

int f(int x) { 
    static int firstArg = x; 
    return firstArg*x; 
} 

C++ 11標準要求初始化函數本地靜態變量是線程安全的*)。更確切地說,它要求只有一個線程初始化變量,並且所有其他線程都等待,直到初始化完成(稍後的讀寫操作當然可以競賽,但由於這是對firstArg的唯一寫訪問,這裏需要同步)。

如果你的編譯器不支持「神奇靜」

下一個最好的方法是使用std::call_once由塞巴斯蒂安·雷德爾,它具有相同的語義建議。

如果通過std::call_once初始化太慢了(它可能使用一個互斥體)和arg是一個內置類型(如int),你可以試試下面的(我沒有做任何測量):

namespace { 
    const int DISALLOWED_VALUE = std::numeric_limits<int>::max(); 
    std::atomic<int> firstArg= DISALLOWED_VALUE; 
} 

int f(int x) { 
    if (firstArg.load(std::memory_order_relaxed) == DISALLOWED_VALUE) { 
     int tmp = DISALLOWED_VALUE; 
     firstArg.compare_exchange_strong(tmp, x); 
    } 
    return firstArg.load(std::memory_order_relaxed)*x; 
} 

DISALLOWED_VALUE是一些不可能作爲有效參數傳遞給f的值。在這種情況下,std::numeric_limits<int>::max()會在與自身相乘時導致整數溢出,因此它不是f的有效參數,因此可以用作firstArg尚未初始化的指示符。

警告:只使用這個,如果你已經驗證,即std::call_once是太慢了具體的工作負載(這幾乎不可能出現的情況),而且這個版本實際上是一個充分的改善。有條件鎖定

由於有

注意/人所提出的各種故障條件鎖定算法一些答案,我也提出了一個正確的手動實現雙重檢查鎖定。

namespace { 
    std::atomic<bool> isInit = false; //has to be atomic 
    std::mutex mux; 
} 
int f(int x) { 
    static int firstArg; 
    if (!isInit.load(std::memory_order_acquire)) { 
     std::lock_guard<std::mutex> lg(mux); 
     if (!isInit.load(std::memory_order_acquire)) { 
      firstArg = x; 
      isInit.store(true,std::memory_order_release); 
     } 
    } 
    return firstArg*x; 
} 

兩個重要的部分是:

  • 做雙重檢查(一旦進入,一旦保護區外)
  • 使用std::atomic爲標誌。否則,無法保證不鎖定的線程遵守商店的標誌和變量的順序。

致謝:
的雙重檢查鎖版是基於cppcon2014呈現由香草薩特和基於由EOF和塞巴斯蒂安的意見/答案的範圍得到擴大。


*)參見例如, this question和從c 14標準(6.7點4)的++最新工作草案:

如果控制進入聲明同時而可變正在初始化,併發執行應等待初始化的完成。

+0

錯誤:'靜態'數據是進程範圍的,所以對於所有線程都是通用的。 –

+4

@BasileStarynkevitch:這是OP想要的,不是嗎? alll線程的值相同。重要的是,初始化是在c + + 11線程安全:http://stackoverflow.com/questions/8102125/is-local-static-variable-initialization-thread-safe-in-c11。所以即使上面的函數同時被多個thrads訪問,也沒有數據競爭。 – MikeMB

+0

@MikeMB我相信你是正確的(儘管你應該返回'firstArg * x')。 –

1

如果您仍然考慮使用某個鎖實現,請嘗試使用自旋鎖。它將線程保留在用戶空間中,並且在操作很快時不會切換到內核空間。

#include "boost/smart_ptr/detail/spinlock.hpp" 

boost::detail::spinlock lock; 

bool first_init = true; 
int first_arg = 0; 

int f(int x) 
{ 
    std::lock_guard<boost::detail::spinlock> guard(lock); 
    if (first_init) 
    { 
    first_arg = x; 
    first_init = false; 
    } 
    return x * first_arg; 
}