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

时间:2017-01-01 14:05:20

标签: c++ multithreading performance 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;
}

1 个答案:

答案 0 :(得分:6)

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

但为什么?

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

Tkinter.OptionMenu

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

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

如果您想避免优化器展开测试循环,您需要将.focus_set()?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 : } 声明为_data。然后,您会注意到由于数据争用,不安全的值不再正确。使用这个修改过的代码运行我自己的测试,我得到安全版本的正确值,并且不安全版本的值总是不同(和错误)。例如:

unsafe_val

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

如果您想让不安全的代码安全但不使用明确的互斥锁,则可以将volatile设为 atomic 。结果将取决于平台(实现可能会很好地为您引入互斥锁)但在与上面相同的机器上,MSVC15处于发布模式,我得到:

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

您唯一必须做的事情是:将变量的原子版本从unsafe_val重命名为safe time:5616282 unsafe time:798851 // still much faster (6 to 7 times in average) val:20971520 val_unsafe:20971520 // but always correct freq2597654 ; - )