并发:C ++ 11内存模型中的原子和易失性

时间:2012-01-11 12:18:28

标签: c++ multithreading concurrency c++11 parallel-processing

全局变量在2个不同核心上的2个并发运行线程之间共享。线程写入和读取变量。对于原子变量,一个线程可以读取过时值吗?每个核心可能在其缓存中具有共享变量的值,并且当一个线程在缓存中写入其副本时,不同核心上的另一个线程可能从其自己的缓存中读取过时值。或者编译器执行强大的内存排序以从其他缓存中读取最新值? c ++ 11标准库具有std :: atomic支持。这与volatile关键字有何不同?在上述场景中,volatile和atomic类型的表现会有何不同?

4 个答案:

答案 0 :(得分:78)

首先,volatile并不意味着原子访问。它专为内存映射I / O和信号处理等设计。与volatile一起使用时,std::atomic完全不必要,除非您的平台文档另有说明,否则volatile与线程之间的原子访问或内存排序无关。

如果您有一个在线程之间共享的全局变量,例如:

std::atomic<int> ai;

然后,可见性和排序约束取决于您用于操作的内存排序参数,以及锁,线程和对其他原子变量的访问的同步效果。

在没有任何其他同步的情况下,如果一个线程将值写入ai,那么没有什么能保证另一个线程在任何给定的时间段内都能看到该值。该标准规定它应“在合理的时间段内”可见,但任何给定的访问都可能返回陈旧的值。

std::memory_order_seq_cst的默认内存排序为所有变量中的所有std::memory_order_seq_cst操作提供单个全局总排序。这并不意味着您不能获得过时的价值,但它确实意味着您获得的价值决定了并且取决于您的操作所处的总订单的位置。

如果您有2个共享变量xy,则最初为零,并且有一个线程将1写入x,另一个写入2写入y,然后是第三个读取两者的线程可能会看到(0,0),(1,0),(0,2)或(1,2),因为操作之间没有排序约束,因此操作可能以任何顺序出现在全球秩序。

如果两个写入都来自同一个线程,x=1之前的y=2和读取线程在y之前读取x,则不再(0,2)一个有效的选项,因为y==2的读取意味着先前对x的写入是可见的。其他3对(0,0),(1,0)和(1,2)仍然是可能的,这取决于2次读取如何与2次写入交错。

如果您使用其他内存排序,例如std::memory_order_relaxedstd::memory_order_acquire,那么约束将进一步放宽,并且单个全局排序不再适用。如果没有额外的同步,线程甚至不必同意两个存储的排序来分离变量。

保证您拥有“最新”值的唯一方法是使用读取 - 修改 - 写入操作,例如exchange()compare_exchange_strong()fetch_add()。读 - 修改 - 写操作有一个额外的约束,它们总是对“最新”值进行操作,因此一系列线程的ai.fetch_add(1)操作序列将返回一系列没有重复或间隙的值。在没有额外约束的情况下,仍然无法保证哪些线程会看到哪些值。

使用原子操作是一个复杂的主题。我建议你阅读很多背景材料,并在用atomics编写生产代码之前检查已发布的代码。在大多数情况下,编写使用锁的代码更容易,效率也不会明显降低。

答案 1 :(得分:30)

volatile和原子操作有不同的背景,和 以不同的意图介绍。

volatile可追溯至日期,主要用于预防 访问内存映射IO时的编译器优化。现代 编译器往往只会抑制volatile的优化, 虽然在某些机器上,这对于内存映射来说还不够 IO。除了信号处理程序的特殊情况,setjmplongjmpgetjmp序列(C标准,在这种情况下) 信号,Posix标准,提供额外的保证),它必须 在没有特殊附加功能的现代机器上被认为是无用的 说明(围栏或内存屏障),硬件可能会重新排序或 甚至压制某些访问。既然你不应该使用setjmp 等。在C ++中,这或多或少留下了信号处理程序,并且在 多线程环境,至少在Unix下,还有更好的 这些解决方案也是如此。如果你是的话,可能还有内存映射的IO 处理内核代码并确保编译器生成 无论平台有什么需要。 (根据 标准,volatile访问是可观察的行为,编译器 必须尊重。但编译器可以定义其含义 “访问”,大多数似乎将其定义为“负载或 存储机器指令已执行“。哪个,就现代而言 处理器,甚至不意味着必然有读或写 在公共汽车上循环,远远低于你期望的顺序。)

鉴于这种情况,C ++标准增加了原子访问权限 跨线程提供一定数量的保证;特别是, 围绕原子访问生成的代码将包含必要的代码 附加说明,以防止硬件重新排序 访问,并确保访问传播到全局 多核计算机上的内核之间共享的内存。 (在某一点上 在标准化工作中,微软建议将这些语义添加到 volatile,我认为他们的一些C ++编译器可以。后 但是,一般而言,讨论委员会的问题 共识 - 包括微软代表 - 就是这样 最好留下volatile的原始意义,并定义 原子类型。)或者只是使用系统级原语,比如 互斥体,执行代码中需要的任何指令。 (他们必须。如果没有一些保证,你就无法实现互斥 关于内存访问的顺序。)

答案 2 :(得分:3)

这里有两件事的基本概要:

1)挥发性关键词:
 告诉编译器这个值可能随时改变,因此它不应该将它缓存在寄存器中。查找旧的&#34;注册&#34; C.&#34; Volatile&#34;中的关键字基本上是&#34; - &#34;运营商注册&#34;&#34;&#34; +&#34;。现代编译器现在进行优化,并注册&#34;用于默认情况下明确请求,因此您只能看到&#39; volatile&#39;了。使用volatile限定符将保证您的处理永远不会使用陈旧值,但仅此而已。

2)原子:
  原子操作在单个时钟周期内修改数据,因此任何其他线程都不可能在这样的更新过程中访问数据。它们通常仅限于硬件支持的任何单时钟组装指令;像++, - 和交换2个指针之类的东西。请注意,这并没有说明ORDER,不同的线程将运行原子指令,只是它们永远不会并行运行。这就是为什么你有所有这些强制订购的附加选项的原因。

答案 3 :(得分:3)

挥发性和原子性有不同的用途。

挥发性: 通知编译器避免优化。此关键字用于意外更改的变量。因此,它可用于表示硬件状态寄存器,ISR变量,多线程应用程序中共享的变量。

原子: 它也用于多线程应用程序。但是,这确保了在多线程应用程序中使用时没有锁定/停止。原子操作没有种族和不可分割。使用的关键方案很少是检查锁是空闲还是使用,原子地添加到值并在多线程应用程序中返回添加值等。