【问题标题】:Is VC++ still broken Sequentially-Consistent-wise?VC++ 是否仍然在顺序一致方面被破坏?
【发布时间】:2014-08-10 09:06:05
【问题描述】:

我看了(大部分)Herb Sutter's the atmoic<> weapons video,我想用样本内的循环来测试“条件锁”。显然,虽然(如果我理解正确的话)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();
}

总结一下代码的思路:全局变量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)

在条件内递增,并在循环后无条件复制回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:             }

程序的输出是0

【问题讨论】:

  • 代码看起来不错。我看不到数据竞赛
  • @JAB C++ volatile,不像Java volatile,与多线程无关。
  • @JAB 它不会为您提供重新排序保证,除非仅访问该变量。真的不是适合这项工作的工具。
  • 我认为问题的标题应该是关于“仍然引入数据竞争”,而不是关于“顺序一致性”。
  • @Hans 什么错误?如果work 他在启动另一个线程之前获取了互斥锁,并且仅在持有锁的同时对其进行写入。如果!work 主线程从不写入或读取a

标签: c++ multithreading visual-c++ concurrency compiler-optimization


【解决方案1】:

“volatile”关键字会阻止这种优化。这正是它的用途:每次使用 'a' 都将完全按照所示进行读取或写入,并且不会以不同的顺序移动到其他 volatile 变量。

互斥锁的实现应该包括特定于编译器的指令,以在该点引起“栅栏”,告诉优化器不要跨该边界重新排序指令。由于实现不是来自编译器供应商,也许这被遗漏了?我从来没有检查过。

由于 'a' 是全局的,我通常认为编译器会更加小心。但是,VS10 不知道线程,所以它不会考虑其他线程会使用它。由于优化器掌握了整个循环执行,它知道从循环内调用的函数不会触及“a”,这就足够了。

我不确定新标准对 volatile 以外的全局变量的线程可见性有何规定。也就是说,是否有一条规则会阻止这种优化(即使该函数可以一直被抓住,所以它知道其他函数不使用全局,它是否必须假设其他线程可以)?

我建议尝试使用编译器提供的 std::mutex 的较新编译器,并检查 C++ 标准和当前草案对此有何评论。我认为以上内容应该可以帮助您了解要查找的内容。

——约翰

【讨论】:

  • 你读过这个问题吗?他已经在使用 std::mutex 了。互斥锁应该引入内存屏障。是的,编译器知道线程。然后,关于 volatile 的建议使用,这种联系被一遍又一遍地驳斥。最多,它是一种解决方法或特定于编译器的方法来创建原子可访问的变量,但这里甚至不涉及任何原子,而是普通的旧互斥体。
  • 是的,我确实阅读了这个问题。包括他在哪里写的,“……我用 boost 而不是 std,……”我不敢苟同:volatile 的含义是标准的且定义明确的。它不仅有用,而且对于线程代码正常工作可能需要的 pre-x11 编译器。它是特定于编译器的,完全可以在没有 volatile 修饰符的情况下工作。当人们不理解它的真正含义时,滥用或使用 volatile 是错误的。 “不了解线程”是指 x11 标准中的详细内容,而不仅仅是遗留的 ad-hoc 功能。
  • 澄清一下,如果没有 volatile,编译器根本没有义务写入内存,或者在任意时间不这样做。如果没有特定于“此变量将跨线程使用”的 C++ 修饰符(我同意 volatile 不是),它是保证它可以被具有排序期望的不同线程使用的唯一可移植方式,并且“它确实做到了“ 期望。太矫枉过正了。通常在实际代码中,编译器无法以这种方式优化非局部变量,因此它主要在没有 volatile 的情况下工作。易失性不会使读取或写入“原子”。
  • 不管编译器是否遵循 C++ 11 或 win32 API 强加的规则,它都需要线程支持,因为 volatile(已定义但留给实现者解释的余地​​)是不够的.就原子访问而言,有问题的编译使用易失性,但这只是一个旁注。无论如何,如果您对线程有适当的编译器支持,则不需要 volatile。那么为什么要建议 volatile 呢?
  • Ulrich,因为 VS10 使用了它自己的线程临时知识,这不包括全局变量可以被多个线程同时访问的想法。正如我所说,一个 C++03 编译器,虽然一个程序可以调用操作系统特性来产生线程,并且可以增强标准库以在这种情况下大部分是安全的,但不会以其他方式为规则赋予线程“可见性”语义, 那么共享变量不能保证在没有 volatile的情况下工作。除了在最终的 volatile 访问中产生正确答案之外,非易失性没有指称语义。
【解决方案2】:

快一个月过去了,微软仍然没有回复bug in MSDN Connect

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

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

建议将a 标记为volatile。这确实防止了这个错误,但只是因为它阻止了优化器执行循环寄存器优化。

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

  1. a 复制到b
  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();
}

这就是汇编代码的样子(&amp;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)  

     ........

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-01-18
    • 2021-08-03
    • 1970-01-01
    • 2021-02-13
    • 2017-08-13
    • 2018-02-26
    • 2010-11-22
    • 1970-01-01
    相关资源
    最近更新 更多