编译器优化是否解决了线程安全问题

Bil*_*Sun 1 c++ performance multithreading thread-safety

我正在编写一个C++多线程代码.在测试不同互斥锁的开销时,我发现线程不安全代码似乎产生了在Visual Studio中使用Release Configuration编译的正确结果,但比使用互斥锁的代码快得多.但是,使用Debug Configuration,结果就是我的预期.我想知道是不是编译器解决了这个问题,或者只是因为在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;
}
Run Code Online (Sandbox Code Playgroud)

Chr*_*phe 6

标准不允许您假设代码默认情况下是线程安全的.在x64的发布模式下编译时,您的代码仍能提供正确的结果.

但为什么 ?

如果查看为代码生成的汇编程序文件,您会发现优化程序只是展开循环并应用常量传播.因此,它不是循环65535次,而是向计数器添加一个常量:

?unsafeThreads@@YAKPEAX@Z PROC              ; unsafeThreads, COMDAT
; 74   :    for (int i = 0; i < 655360; i++) {
    add QWORD PTR ?val_unsafe@@3_KA, 655360 ; 000a0000H   <======= HERE 
; 75   :        val_unsafe++;
; 76   :    }
; 77   :    return 0;
    xor eax, eax                             
; 78   : }
Run Code Online (Sandbox Code Playgroud)

在这种情况下,在每个线程中使用单个且非常快的指令,获得数据竞争的可能性要小得多:最有可能一个线程在下一个线程启动之前已经完成.

如何查看基准测试的预期结果?

如果要避免优化器展开测试循环,则需要声明_dataunsafe_valas volatile.然后,您会注意到由于数据争用,不安全的值不再正确.使用这个修改过的代码运行我自己的测试,我得到安全版本的正确值,并且不安全版本的值总是不同(和错误).例如:

safe time:5672583
unsafe time:145092                   // <=== much faster
val:20971520
val_unsafe:3874844                   // <=== OUCH !!!!
freq: 2597654
Run Code Online (Sandbox Code Playgroud)

想要使您的不安全代码安全吗?

如果你想让不安全的代码安全但不使用显式的互斥锁,你可以做unsafe_val一个atomic.结果将取决于平台(实现可以很好地为您引入互斥锁)但是在上面的同一台机器上,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
Run Code Online (Sandbox Code Playgroud)

你还必须做的唯一事情是:将变量的原子版本重命名unsafe_valalso_safe_val;-)