【问题标题】:Are volatile reads and writes atomic on Windows+VisualC?Windows+VisualC 上的 volatile 读写是原子的吗?
【发布时间】:2011-10-23 20:36:03
【问题描述】:

本网站上有几个问题询问是否可以使用volatile 变量进行原子/多线程访问:例如,请参阅herehereor here

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

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

我最近answered 并在volatile 上引用了official MSDN docs

微软特定

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

  • 对 volatile 对象的写入(volatile write)具有 Release 语义; 对 全局或静态对象? 的引用发生在写入之前 指令序列中的易失性对象将在此之前发生 volatile 写入编译后的二进制文件。
  • 对易失性对象的读取(易失性读取)具有 Acquire 语义;一个参考 全局或静态对象?在读取易失性内存后发生 操作说明 序列将在编译后的二进制文件中进行易失性读取之后发生。

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

[强调我的]

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

但是,在comment to my answer 中,用户Hans Passant 写道“那篇MSDN 文章很不幸,它是大错特错。你不能用volatile 实现锁,即使是微软的版本。(。 ..)"


请注意:MSDN 中给出的示例 看起来很可疑,因为您通常无法在没有原子exchange 的情况下实现锁。 (同样pointed out by Alex。)这仍然留下了问题。这篇 MSDN 文章中给出的其他信息的有效性,尤其是像 herehere 这样的用例。)


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

更重要的是,上面引用的volatile 文档以某种方式暗示了“全局或静态对象”,我认为“真实”acquire/release semantics 应该适用于所有值。

回到问题

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

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

请用可验证的信息、链接、测试用例备份您的答案!

【问题讨论】:

  • 屏障语义并不意味着指令序列的原子性。特别是序列加载、添加、存储不是原子的。
  • @Alex:问题不在于序列,而在于单次读取或写入。
  • @Martin:即使在这种情况下,这也取决于硬件为您提供的功能。 VS 不会在volatile 访问周围添加lock,这意味着只有当底层处理器指令是原子的时它才会是原子的。 IE。例如,它不会用于未对齐的整数类型,也不会用于 32 位平台中的 64 位整数。
  • @Martin:在 32 位 Windows 操作系统中,使用 uint32_t volatile 引用,访问不是原子的。请注意,您引用的 MSDN 部分并没有说它保证操作的原子性,它只是说它提供的语义可能足以用于内存锁。我希望我有更多的时间来写一个完整的答案。
  • 嗯...我刚刚注意到我的最后一条评论是错误的...变量将是uint64_t,因为操作不是原子的。 Intel/AMD保证32bit模式下32bit读写原子性,64bit模式下64bit读写原子性。

标签: c++ visual-c++ atomic volatile memory-fences


【解决方案1】:

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

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

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

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

【讨论】:

  • “假设你满足对齐要求等或课程” - 你能详细说明吗?编译器会确保满足简单 32/64 位类型的对齐要求,还是需要一些额外的措施?
  • 是的,正常使用应该没有问题,你必须明确地写一些奇怪的东西来防止对齐是合适的。 (只是指出,如果您使用 #pragma pack 或狡猾的强制转换来获得未对齐的变量,那么 volatile 将无济于事)
  • “假设您满足对齐要求”将使英特尔平台中的访问原子化,而与操作系统无关。原子性与波动性无关。 VS C++ 编译器中的额外约束仅影响指令的顺序和确保编译器和 CPU 都不会执行特定的指令重新排序的内存栅栏。并且不能保证在 32 位平台上对 64 位 int 进行原子读/写。
  • 是的,这就是我的意思,虽然不是我说的那样。我将编辑我的评论以避免混淆。
【解决方案2】:

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

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

【讨论】:

  • "only ... to x86/AMD64" - 这意味着在 Itanium/IA-64 上的 Windows 上运行的程序不能依赖这种原子性?
  • 我相信 Itanium 具有相同的对齐读/写行为。不过,我不了解 ARM,它是另一个 Windows 平台。
【解决方案3】:

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

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

如果你考虑一下:

void foo(int volatile*);

它说:

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

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

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

【讨论】:

  • 很好的信息!我唯一不明白的是为什么 Interlocked* 函数需要指向 volatile 限定变量的指针。 :-)
  • 这个答案:stackoverflow.com/questions/6397662/… 解释了为什么这些参数被标记为 volatile。
  • 这是上个月的答案。
【解决方案4】:

关键可能是允许类似的东西

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,它提供了合适的内存模型和显式屏障。

【讨论】:

  • 很好的例子。 Tobias 在他的回答中写道“仅适用于……x86/AMD64”,所以我仍然不清楚这些语义是否适用于所有运行 Windows 的硬件平台。
  • 这种模式被打破了。确实,微软在某一时刻正在扩展volatile 的语义以完成这项工作。但是,我不知道他们实际上走了多远。他们向标准委员会提出了这个扩展,经过一些讨论,他们的代表承认这不是一个好主意,并撤回了该提案。当然,在微软世界之外,这根本行不通。编译器可以(并且在某些情况下很可能这样做)在写入 volatile 之后在构造函数中移动写入。
  • @James:确实。我在 MSDN 页面中读到的关于 volatile 的内容是,无论编译器(和 CPU!)如何重新排序,对 instance 的写入都将在构造之后发生,并且从 instance 读取也将正常运行。在 C++0x 中,您将使用 std::atomic<singleton*>(使用默认的完整屏障语义,或更具体的获取或释放)
  • @Alexandre C 是的。基本上,微软的提议是赋予volatilestd::atomic<> 的语义。委员会不同意这一点的原因是它为 volatile 的现有用途(通常是内存映射 IO)增加了太多开销。
【解决方案5】:

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

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

• 读或写一个字节

• 读取或写入在 16位边界

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

奔腾处理器(以及之后的更新处理器)保证 将始终进行以下额外的内存操作 原子输出:

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

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

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

• 未对齐的 16-、32-、 以及对适合缓存行的缓存内存进行 64 位访问

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

【讨论】:

  • “意味着 volatile 只会用于防止编译器缓存和指令重新排序”——那么您是在说 MSDN 文档在撒谎?
  • @Martin:我说它们具有误导性,因为它们依赖于 x86 硬件来处理原子部分(我已经检查过它们不会发出原子操作来处理 volatile 合格变量)
  • 谢谢 - 这也是我应该做的 - 检查发出的代码 :-)
  • Hmm ... 从 JohnB 的回答中,我现在了解到 VC++ volatile 规范的 ordering 部分似乎也很相关。 WRT。原子性 - Windows 目前在这些平台的 x86、x64(AMD64) 和 Itanum(IA-64) 上运行,哪个不保证原子对齐访问?
  • @Martin:某些移动设备上的 Windows 可能会因没有对齐的原子读/写而受到影响,但是,如果您查看 Windows 同步原语,您会发现它们都使用基于 LOCK说明,因此他们要么不信任自己的文档,要么原子性可能仅基于每个核心
猜你喜欢
  • 2020-02-05
  • 2016-07-08
  • 2010-09-08
  • 1970-01-01
  • 2011-01-16
  • 2018-03-03
  • 1970-01-01
  • 1970-01-01
  • 2010-09-08
相关资源
最近更新 更多