代码之家  ›  专栏  ›  技术社区  ›  Jay D

互斥体的存在是否有助于消除易失性关键字?

  •  9
  • Jay D  · 技术社区  · 16 年前

    我有一个多R/W锁类,用于保存读、写和挂起读、挂起写计数器。互斥体保护它们不受多个线程的影响。

    我的问题是,我们是否仍然需要将计数器声明为volatile,以便编译器在进行优化时不会弄乱它。

    或者编译器是否考虑到计数器由互斥锁保护。

    我理解互斥体是用于同步的运行时机制,“volatile”关键字是编译器在进行优化时执行正确操作的编译时指示。

    当做, -杰伊。

    5 回复  |  直到 14 年前
        1
  •  15
  •   tony    16 年前

    这里有两个基本上不相关的项目,总是混淆。

    • 不稳定的
    • 线程、锁、内存屏障等。

    volatile用来告诉 编译程序 产生从存储器而不是从寄存器中读取变量的代码。不要重新排列代码。一般来说,不优化或采取“捷径”。

    记忆屏障(由互斥体、锁等提供),如另一个答案中所引用的Herb Sutter,用于防止 中央处理器 从重新排序读/写内存请求,不管编译器如何说。也就是说,不要优化,不要走捷径——在CPU级别。

    相似,但事实上却截然不同。

    在您的情况下,在大多数锁定情况下,不需要volatile的原因是 函数调用 是为了锁定而制造的。IE:

    影响优化的普通函数调用:

    external void library_func(); // from some external library
    
    global int x;
    
    int f()
    {
       x = 2;
       library_func();
       return x; // x is reloaded because it may have changed
    }
    

    除非编译器可以检查library_func()并确定它不接触x,否则它将在返回时重新读取x。这甚至没有挥发性。

    线程:

    int f(SomeObject & obj)
    {
       int temp1;
       int temp2;
       int temp3;
    
       int temp1 = obj.x;
    
       lock(obj.mutex); // really should use RAII
          temp2 = obj.x;
          temp3 = obj.x;
       unlock(obj.mutex);
    
       return temp;
    }
    

    在为temp1读取obj.x之后,编译器将为temp2重新读取obj.x,这不是因为锁的魔力,而是因为它不确定lock()是否修改了obj。您可能会将编译器标志设置为积极优化(没有别名等),因此不会重新读取x,但随后一堆代码可能会开始失败。

    对于temp3,编译器(希望)不会重新读取obj.x。 如果由于某种原因obj.x可以在temp2和temp3之间更改,那么您将使用volatile(并且您的锁定将被破坏/无用)。

    最后,如果您的lock()/unlock()函数以某种方式是内联的,那么编译器可能会对代码进行评估,并看到obj.x没有被更改。但我保证这里有两件事: -内联代码最终调用一些操作系统级锁函数(从而阻止计算),或者 -您可以调用一些ASM内存屏障指令(即封装在内联函数中的指令,如“interlockedcompareexchange”),编译器将识别这些指令,从而避免重新排序。

    编辑:P.S.我忘了提到——对于pthread s的东西,一些编译器被标记为“posix兼容”,这意味着,除其他外,它们将识别pthread_u函数,并且不会对它们进行糟糕的优化。IE即使C++标准还没有提到线程,但这些编译器(至少是最低限度)。

    所以,简短的回答

    你不需要反复无常。

        2
  •  13
  •   Michael Burr    16 年前

    从Herb Sutter的文章“使用关键部分(最好是锁)来消除种族”( http://www.ddj.com/cpp/201804238 )以下内容:

    因此,为了使重新排序转换有效,它必须遵守关键部分的一个关键规则来尊重程序的关键部分:代码不能移出关键部分。(代码总是可以移入的。)我们通过要求对任何关键部分的开始和结束使用对称的单向边界语义来实现这一黄金法则,如图1中的箭头所示:

    • 进入一个关键部分是一个获取操作,或者一个隐式的获取围栏:代码永远不能向上穿过围栏,也就是说,从围栏后的原始位置移动到围栏前执行。但是,以源代码顺序出现在围栏之前的代码可以很高兴地向下越过围栏以稍后执行。
    • 退出一个关键部分是一个释放操作,或者一个隐式的释放界限:这只是一个相反的要求,即代码不能向下穿过界限,只能向上。它保证看到最终版本写入的任何其他线程也将看到它之前的所有写入。

    因此,当一个关键部分被输入和退出时(并且术语critical section在它的一般意义上被使用,不一定在win32意义上被 CRITICAL_SECTION 结构-关键部分可以由其他同步对象保护)必须遵循正确的获取和发布语义。因此,只要共享变量只在受保护的关键部分内被访问,就不必将其标记为易失性变量。

        3
  •  5
  •   codefool    16 年前

    volatile用于通知优化器始终加载位置的当前值,而不是将其加载到寄存器中并假定它不会更改。当使用双端口内存位置或可以从线程外部的源实时更新的位置时,这是最有价值的。

    互斥体是一种运行时操作系统机制,编译器实际上对它一无所知,所以优化器不会考虑到这一点。它将阻止多个线程一次访问计数器,但这些计数器的值仍然会发生更改,即使互斥体有效。

    所以,您将var标记为volatile是因为它们可以从外部修改,而不是因为它们在互斥保护中。

    保持不稳定。

        4
  •  4
  •   Community Mohan Dere    9 年前

    虽然这可能取决于您使用的线程库,但我的理解是任何一个合适的库都将 需要使用 volatile .

    在Pthreads, for example ,使用互斥将确保数据正确地提交到内存中。

    编辑: 我在此背书 tony's answer 比我自己的强。

        5
  •  3
  •   Black    14 年前

    您仍然需要“volatile”关键字。

    互斥体阻止计数器并发访问。

    “volatile”告诉编译器实际使用计数器 而不是将其缓存到CPU寄存器中 由并发线程更新)。