VC++仍然按顺序 - 一致吗?

Asa*_*saf 9 c++ concurrency multithreading compiler-optimization visual-c++

我看了(大部分)Herb Sutter的atmoic <>武器视频,我想用样本中的循环来测试"条件锁定".显然,虽然(如果我理解正确的话)C++ 11标准说下面的例子应该正常工作并且顺序一致,但事实并非如此.

在您继续阅读之前,我的问题是:这是正确的吗?编译器坏了吗?我的代码是否被破坏 - 我在这里遇到了一个我错过的竞争条件吗?我该如何绕过这个?

我尝试了3种不同版本的Visual C++:VC10专业版,VC11专业版和VC12 Express版(== Visual Studio 2013 Desktop Express).

下面是我用于Visual Studio 2013的代码.对于其他版本,我使用boost而不是std,但想法是一样的.

#include <iostream>
#include <thread>
#include <mutex>

int a = 0;
std::mutex m;

void other()
{
    std::lock_guard<std::mutex> l(m);
    std::this_thread::sleep_for(std::chrono::milliseconds(2));
    a = 999999;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << a << "\n";
}

int main(int argc, char* argv[])
{
    bool work = (argc > 1);

    if (work)
    {
        m.lock();
    }

    std::thread th(other);
    for (int i = 0; i < 100000000; ++i)
    {
        if (i % 7 == 3)
        {
            if (work)
            {
                ++a;
            }
        }
    }

    if (work)
    {
        std::cout << a << "\n";
        m.unlock();
    }

    th.join();
}
Run Code Online (Sandbox Code Playgroud)

总结代码的概念:全局变量a受全局互斥锁的保护m.假设没有命令行参数(argc==1),运行的线程other()是唯一一个应该访问全局变量的线程a.

程序的正确输出是打印999999.

但是,由于编译器循环优化(使用寄存器进行循环增量并在循环结束时将值复制回a),a即使它不应该由程序集修改.

这发生在所有3个VC版本中,尽管在VC12的这个代码示例中,我不得不进行一些调用sleep()以使其中断.

这里是一些汇编代码(a这次运行的地址是0x00f65498):

循环初始化 - 将值a复制到edi

    27:     for (int i = 0; i < 100000000; ++i)
00F61543  xor         esi,esi  
00F61545  mov         edi,dword ptr ds:[0F65498h]  
00F6154B  jmp         main+0C0h (0F61550h)  
00F6154D  lea         ecx,[ecx]  
    28:     {
    29:         if (i % 7 == 3)
Run Code Online (Sandbox Code Playgroud)

在条件内增加,并在循环后a无条件地复制回位置

    30:         {
    31:             if (work)
00F61572  mov         al,byte ptr [esp+1Bh]  
00F61576  jne         main+0EDh (0F6157Dh)  
00F61578  test        al,al  
00F6157A  je          main+0EDh (0F6157Dh)  
    32:             {
    33:                 ++a;
00F6157C  inc         edi  
    27:     for (int i = 0; i < 100000000; ++i)
00F6157D  inc         esi  
00F6157E  cmp         esi,5F5E100h  
00F61584  jl          main+0C0h (0F61550h)  
    32:             {
    33:                 ++a;
00F61586  mov         dword ptr ds:[0F65498h],edi  
    34:             }
Run Code Online (Sandbox Code Playgroud)

而程序的输出是0.

Asa*_*saf 0

近一个月过去了,微软仍然没有对MSDN Connect 中的错误做出回应。

总结以上评论(以及一些进一步的测试),显然它也发生在 VS2013 专业版中,但该错误仅发生在为 Win32 构建时,而不是为 x64 构建时。x64 中生成的汇编代码不存在此问题。所以看来这是优化器中的一个错误,并且此代码中没有竞争条件。

显然这个错误也发生在 GCC 4.8.1 中,但不会发生在 GCC 4.9 中。(感谢VoonosidChris Dodd进行的所有测试)。

建议标记avolatile。这确实可以防止错误,但只是因为它阻止优化器执行循环寄存器优化。

我找到了另一个解决方案:添加另一个局部变量b,如果需要(并且处于锁定状态),请执行以下操作:

  1. 复制ab
  2. b循环中增量
  3. a如果需要的话复制回

优化器用寄存器替换局部变量,因此代码仍然是优化的,但a在需要时才进行来自和到的复制,并且处于锁定状态。

这是新main()代码,其中箭头标记了更改的行。

int main(int argc, char* argv[])
{
    bool work = (argc == 1);

    int b = 0;          // <----

    if (work)
    {
        m.lock();
        b = a;          // <----
    }

    std::thread th(other);
    for (int i = 0; i < 100000000; ++i)
    {
        if (i % 7 == 3)
        {
            if (work)
            {
                ++b;    // <----
            }
        }
    }

    if (work)
    {
        a = b;          // <----
        std::cout << a << "\n";
        m.unlock();
    }

    th.join();
}
Run Code Online (Sandbox Code Playgroud)

汇编代码如下所示(&a == 0x000744b0b替换为edi):

    21:     int b = 0;
00071473  xor         edi,edi  
    22: 
    23:     if (work)
00071475  test        bl,bl  
00071477  je          main+5Bh (07149Bh)  
    24:     {
    25:         m.lock();

         ........

00071492  add         esp,4  
    26:         b = a;
00071495  mov         edi,dword ptr ds:[744B0h]  
    27:     }
    28: 

         ........

    33:         {
    34:             if (work)
00071504  test        bl,bl  
00071506  je          main+0C9h (071509h)  
    35:             {
    36:                 ++b;
00071508  inc         edi  
    30:     for (int i = 0; i < 100000000; ++i)
00071509  inc         esi  
0007150A  cmp         esi,5F5E100h  
00071510  jl          main+0A0h (0714E0h)  
    37:             }
    38:         }
    39:     }
    40: 
    41:     if (work)
00071512  test        bl,bl  
00071514  je          main+10Ch (07154Ch)  
    42:     {
    43:         a = b;
    44:        std::cout << a << "\n";
00071516  mov         ecx,dword ptr ds:[73084h]  
0007151C  push        edi  
0007151D  mov         dword ptr ds:[744B0h],edi  
00071523  call        dword ptr ds:[73070h]  
00071529  mov         ecx,eax  
0007152B  call        std::operator<<<std::char_traits<char> > (071A80h)  

     ........
Run Code Online (Sandbox Code Playgroud)

这可以保持优化并解决(或解决)问题。