在C ++中,我们有关键字volatile
和atomic
类。它们之间的区别在于volatile不能保证线程安全的并发读写,而只是确保编译器不会在高速缓存中存储变量的值,而是从内存中加载变量,而atomic保证线程安全的并发读写。
我们知道,原子读取操作不可分割,即,当一个或多个线程读取变量值时,两个线程都无法将新值写入变量,因此我认为我们总是读取最新值,但是我不确定:)
所以,我的问题是:如果我们声明原子变量,我们是否总是获得调用load()
操作的变量的最新值?
答案 0 :(得分:0)
当我们谈论现代体系结构上的内存访问时,我们通常会忽略从中读取值的“确切位置”。
读取操作可以从缓存(L0 / L1 / ...),RAM甚至硬盘驱动器(例如,交换内存)中获取数据。
这些关键字告诉编译器访问数据时要使用哪些汇编操作。
易失性
一个关键字,告诉编译器始终从内存而不是从寄存器读取变量的值。
此“内存”仍然可以是高速缓存,但是,如果高速缓存中的“地址”被认为是“脏”的,这意味着该值已由其他处理器更改,则将重新加载该值。
这可以确保我们永远不会读取过时的值。
但是,如果类型声明volatile
不是原始类型,则其读/写操作本质上是原子的(就读/写它的汇编指令而言),我们可能会读取一个中间值(写者设法在读者阅读时只写了一半的字节。
原子
并且编译器看到load
(读)操作,除了使用原子操作外,它基本上完成了与volatile
值完全相同的操作(这意味着我们永远不会读取一个中间值)。
那么,有什么区别?
区别在于跨CPU写操作。 使用volatile变量时,如果CPU 1设置了该值,而CPU 2读取了该值,则读取器可能会读取旧的值。
但是,怎么可能呢? volatile关键字承诺我们不会读取过时的值!
好吧,那是因为作者没有发布价值!尽管读者试图阅读它,但它会阅读旧的。
当编译器偶然遇到原子变量的store
(写)操作时,它会:
宣布之后,所有CPU都知道它们应该重新读取变量的值,因为其缓存将被标记为“脏”。
此机制与对文件执行的操作非常相似。当您的应用程序写入硬盘驱动器上的文件时,其他应用程序可能会或可能不会看到新信息,具体取决于您的应用程序是否将数据刷新到硬盘驱动器上。
如果未刷新数据,则它仅驻留在应用程序缓存中的某个位置,并且仅可见。刷新文件后,打开文件的任何人都将看到新状态。
答案 1 :(得分:0)
如果我们声明原子变量,是否总是得到的最新值 调用load()操作的变量?
是的,对于最新的某些定义。
并发性的问题是不可能以通常的方式争论事件的顺序。这是由于硬件的一个基本限制,即跨多个内核建立全局操作顺序的唯一方法是将它们串行化(并消除过程中并行计算的所有性能优势)。
现代处理器提供的是一种选择加入机制,可以在某些操作之间重新建立顺序。原子是该机制的语言级抽象。设想一个场景,其中两个atomic<int>
和a
和b
在线程之间共享(让我们进一步假设它们已初始化为0
):
// thread #1
a.store(1);
b.store(1);
// thread #2
while(b.load() == 0) { /* spin */ }
assert(a.load() == 1);
这里的断言肯定会成立。线程#2将观察a
的“最新”值。
该标准没有讨论的是什么时候确切的循环会观察到b
的值从0
变为1
。我们知道它会在线程#1写入之后的某个时间发生,并且我们也知道它将在写入a
之后发生。但是我们不知道会持续多久。
这种推理由于以下事实而变得更加复杂:在进行某些写操作时,允许不同的线程不同意。如果切换到weaker memory ordering,则一个线程可能会观察到对不同原子变量的写操作,而这些原子变量却以与另一个线程所观察到的不同的不同顺序发生。