11

该站点上有几个问题询问是否可以使用volatile变量进行原子/多线程访问:例如,请参见此处此处此处

现在,符合 C(++) 标准的答案显然是no

但是,在 Windows 和 Visual C++ 编译器上,情况似乎并不那么清楚。

我最近回答并引用了官方MSDN 文档volatile

微软特定

声明为 volatile 的对象是 (...)

  • 对 volatile 对象的写入(volatile write)具有 Release 语义;对全局或静态对象的引用在指令序列中写入易失性对象之前发生的事件将发生在已编译二进制文件中的易失性写入之前。
  • 对 volatile 对象的读取(volatile read)具有 Acquire 语义;对全局或静态对象的引用 在指令序列中读取易失性存储器之后发生的情况将发生在已编译二进制文件中的易失性读取之后。

这允许易失性对象用于多线程应用程序中的内存锁定和释放。

[强调我的]

现在,阅读本文,在我看来,MS 编译器std::atomic将像在即将到来的 C++11 标准中一样处理 volatile 变量。

然而,在对我的回答的评论中,用户Hans Passant写道:“那篇 MSDN 文章非常不幸,它是大错特错。你不能用 volatile 实现锁,即使是微软的版本也不能。(...)”


请注意:MSDN 中给出的示例看起来很可疑,因为您通常无法在没有 atomic exchange的情况下实现锁。(正如亚历克斯也指出的那样。)这仍然留下了问题。对于这篇 MSDN 文章中给出的其他信息的有效性,尤其是对于像这里这里这样的用例。)


此外,还有 The Interlocked* 函数的文档,尤其是InterlockedExchange使用volatile(!?)变量并进行原子读写。(请注意,我们对 SO 提出的一个问题—— 何时应使用 InterlockedExchange? ——并没有权威地回答只读或只写原子访问是否需要此函数。)

更重要的是,volatile上面引用的文档以某种方式暗示了“全局或静态对象”,我认为“真正的”获取/释放语义应该适用于所有值。

回到问题

在 Windows 上,使用 Visual C++ (2005 - 2010),将声明一个 (32bit?int?) 变量以volatile允许对该变量进行原子读取和写入 - 或不?

对我来说特别重要的是,这应该在 Windows/VC++ 上保持(或不)独立于程序运行的处理器或平台。(也就是说,在 Itanum2 上运行的是 WinXP/32bit 还是 Windows 2008R2/64bit?)

请用可验证的信息、链接、测试用例来支持您的答案!

4

5 回答 5

6

是的,它们在 windows/vc++ 上是原子的(假设您满足对齐要求等或课程)

但是,对于锁,您需要进行原子测试和设置,或者比较和交换指令或类似指令,而不仅仅是原子更新或读取。

否则没有办法测试锁在一个不可分割的操作中声明它。

编辑:如下所述,32 位或更低的 x86 上的所有对齐内存访问无论如何都是原子的。关键是 volatile 使内存访问有序。(感谢您在评论中指出这一点)

于 2011-08-10T07:50:03.360 回答
3

自 Visual C++ 2005 起,volatile 变量是原子的。但这仅适用于此类特定的编译器和 x86/AMD64 平台。例如,PowerPC 可能会重新排序内存读/写,并且需要读/写屏障。我不熟悉 gcc 类编译器的语义,但无论如何使用 volatile 进行原子操作都不是很便携。

参考,见第一句话“微软特定”:http: //msdn.microsoft.com/en-us/library/12a04hfd%28VS.80%29.aspx

于 2011-08-10T07:56:13.577 回答
1

有点题外话了,不过还是先试试吧。

...有 The Interlocked* 函数的文档,尤其是 InterlockedExchange,它采用 volatile(!) 变量...

如果您考虑一下:

void foo(int volatile*);

是不是说:

  • 参数必须是指向 volatile int 的指针,或者
  • 参数也可能是指向 volatile int 的指针?

后者是正确的答案,因为该函数可以同时传递指向 volatile 和 non-volatile int 的指针。

因此,InterlockedExchangeX()具有其参数 volatile-qualified 的事实并不意味着它必须仅对 volatile 整数进行操作。

于 2011-08-10T08:30:57.817 回答
1

关键可能是允许像

singleton& get_instance()
{
    static volatile singleton* instance;
    static mutex instance_mutex;

    if (!instance)
    {
        raii_lock lock(instance_mutex);

        if (!instance) instance = new singleton;
    }

    return *instance;
}

instance如果在初始化完成之前写入会中断。使用 MSVC 语义,您可以保证一旦看到instance != 0,对象就已经完成初始化(如果没有适当的屏障语义,即使使用传统的 volatile 语义也不会出现这种情况)。

这种双重检查锁(反)模式实际上很常见,如果你不提供屏障语义,就会被破坏。但是,如果可以保证对volatile变量的访问是获取 + 释放障碍,那么它就可以工作。

不要依赖这种自定义语义volatile。我怀疑这是为了不破坏现有代码库而引入的。无论如何,不​​要按照 MSDN 的例子写锁。它可能不起作用(我怀疑您是否可以仅使用障碍来编写锁:您需要原子操作——CAS、TAS 等——为此)。

编写双重检查锁模式的唯一可移植方法是使用 C++0x,它提供了合适的内存模型和显式屏障。

于 2011-08-10T08:47:28.237 回答
1

在 x86 下,这些操作保证是原子的,不需要基于 LOCK 的指令,例如Interlocked*(参见英特尔的开发人员手册 3A 第 8.1 节):

基本的内存操作将始终以原子方式执行:

• 读取或写入一个字节

• 读取或写入在 16 位边界上对齐的字

• 读取或写入在 32 位边界上对齐的双字

Pentium 处理器(以及之后的更新处理器)保证以下额外的内存操作将始终以原子方式执行:

• 读取或写入在 64 位边界上对齐的四字

• 对适合 32 位数据总线的未缓存内存位置进行 16 位访问

P6 系列处理器(以及之后的更新处理器)保证以下附加内存操作将始终以原子方式执行:

• 未对齐的 16 位、32 位和 64 位访问适合高速缓存行的高速缓存内存

这意味着volatile仅用于防止编译器缓存和指令重新排序(MSVC 不会为 volatile 变量发出原子操作,它们需要显式使用)。

于 2011-08-10T08:55:49.043 回答