2017-01-01 110 views
1

我在寫一個C++多線程代碼。在測試不同互斥鎖的開銷時,我發現線程不安全的代碼看起來會產生用Visual Studio中的發佈配置編譯的正確結果,但比具有互斥鎖的代碼快得多。然而,使用調試配置的結果是我所期望的。我在想,如果是編譯器解決了這個問題,或者僅僅是因爲在Release配置中編譯的代碼運行速度如此之快以至於兩個線程從不在同一時間訪問內存呢?編譯器優化是否解決了線程安全問題?

我的測試代碼粘貼如下。

class Mutex { 
public: 
unsigned long long _data; 

bool tryLock() { 
    return mtx.try_lock(); 
} 

inline void Lock() { 
    mtx.lock(); 
} 
inline void Unlock() { 
    mtx.unlock(); 
} 
void safeSet(const unsigned long long &data) { 
    Lock(); 
    _data = data; 
    Unlock(); 
} 
Mutex& operator++() { 
    Lock(); 
    _data++; 
    Unlock(); 
    return (*this); 
} 
Mutex operator++(int) { 
    Mutex tmp = (*this); 
    Lock(); 
    _data++; 
    Unlock(); 
    return tmp; 
} 
Mutex() { 
    _data = 0; 
} 
private: 
std::mutex mtx; 
Mutex(Mutex& cpy) { 
    _data = cpy._data; 
} 
}val; 

static DWORD64 val_unsafe = 0; 
DWORD WINAPI safeThreads(LPVOID lParam) { 
for (int i = 0; i < 655360;i++) { 
    ++val; 
} 
return 0; 
} 
DWORD WINAPI unsafeThreads(LPVOID lParam) { 
for (int i = 0; i < 655360; i++) { 
    val_unsafe++; 
} 
return 0; 
} 

int main() 
{ 
val._data = 0; 
vector<HANDLE> hThreads; 
LARGE_INTEGER freq, time1, time2; 
QueryPerformanceFrequency(&freq); 
QueryPerformanceCounter(&time1); 
for (int i = 0; i < 32; i++) { 
    hThreads.push_back(CreateThread(0, 0, safeThreads, 0, 0, 0)); 
} 
for each(HANDLE handle in hThreads) 
{ 
    WaitForSingleObject(handle, INFINITE); 
} 
QueryPerformanceCounter(&time2); 
cout<<time2.QuadPart - time1.QuadPart<<endl; 
hThreads.clear(); 
QueryPerformanceCounter(&time1); 

for (int i = 0; i < 32; i++) { 
    hThreads.push_back(CreateThread(0, 0, unsafeThreads, 0, 0, 0)); 
} 
for each(HANDLE handle in hThreads) 
{ 
    WaitForSingleObject(handle, INFINITE); 
} 
QueryPerformanceCounter(&time2); 
cout << time2.QuadPart - time1.QuadPart << endl; 

hThreads.clear(); 
cout << val._data << endl << val_unsafe<<endl; 
cout << freq.QuadPart << endl; 
return 0; 
} 
+7

不,一般不能自動解決互斥問題。你看到的只是一個巧合。 – Barmar

+0

「似乎工作」是未定義行爲的一種可能表現形式。 –

回答

6

該標準不允許您假設代碼默認是線程安全的。在x64的發佈模式下編譯時,您的代碼仍會給出正確的結果。

但是爲什麼?

如果你看看爲你的代碼生成的彙編文件,你會發現優化器簡單地展開了循環並應用了常量傳播。因此,而不是循環65535次,它只是增加了一個常量到您的計數器:

[email protected]@[email protected] PROC    ; unsafeThreads, COMDAT 
; 74 : for (int i = 0; i < 655360; i++) { 
    add QWORD PTR [email protected]@3_KA, 655360 ; 000a0000H <======= HERE 
; 75 :  val_unsafe++; 
; 76 : } 
; 77 : return 0; 
    xor eax, eax        
; 78 : } 

在這種情況下,在每個線程單和非常快速的指令,它可能更得到一個數據的比賽:最有可能一個線程在下一個啓動之前已經完成。

如何查看基準的預期結果?

如果你想避免優化器展開你的測試循環,你需要聲明_dataunsafe_valvolatile。然後您會注意到由於數據競爭導致不安全的值不再正確。使用此修改後的代碼運行我自己的測試,我會爲安全版本獲取正確的值,並且始終爲不安全版本提供不同(和錯誤)的值。例如:

safe time:5672583 
unsafe time:145092     // <=== much faster 
val:20971520 
val_unsafe:3874844     // <=== OUCH !!!! 
freq: 2597654 

想讓您的不安全代碼安全嗎?

如果你想使你的不安全的代碼安全的,但不使用一個明確的互斥體,你可以只讓unsafe_valatomic。其結果將是與平臺相關的(實現很可能引入一個互斥你),但在同一臺機器比上面上,與MSVC15在發佈模式下,我得到:

safe time:5616282 
unsafe time:798851     // still much faster (6 to 7 times in average) 
val:20971520 
val_unsafe:20971520     // but always correct 
freq2597654 

的唯一的事情,你那麼還必須做:將變量的原子版本從unsafe_val重命名爲also_safe_val ;-)

+0

比較顯式使用互斥體和使用原子在不同平臺上的彙編代碼會很有趣。 – mgarey

+0

@ mgarey確實。原子版本使用原子[LOCK XADD指令](http://stackoverflow.com/a/8891781/3723423),而互斥體版本依賴於2庫調用('__imp__Mtx_lock' /'__imp__Mtx_unlock'),每個都需要加載互斥體的地址作爲調用的參數,所以它顯然是一些指令(在這種情況下)。 – Christophe

+0

這解釋了我的速度差異。我懷疑,如果原子代碼比簡單的add指令更復雜(做更多的計算,可能修改或讀取共享資源),那麼必須使用互斥量。 – mgarey