2017-01-01 90 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