【问题标题】:Does atomic read guarantees reading of latest value?原子读取是否保证读取最新值?
【发布时间】:2018-10-29 17:56:17
【问题描述】:

在 C++ 中,我们有关键字 volatileatomic 类。两者的区别在于 volatile 不保证线程安全的并发读写,只是保证编译器不会将变量的值存储在缓存中,而是从内存中加载变量,而 atomic 保证线程安全的并发读写。

众所周知,原子读取操作不可分割,即两个线程都不能向变量写入新值,而一个或多个线程读取变量的值,所以我认为我们总是读取最新的值,但是我不确定:)

所以,我的问题是:如果我们声明原子变量,我们是否总是得到调用load() 操作的变量的最新值

【问题讨论】:

  • "但只是确保编译器不会将变量的值存储在缓存中,而是从内存中加载变量" - 不是这样,真的。它只是确保“严格按照抽象机的规则”访问值。 C++ 标准及其抽象机器对缓存一无所知。所以你不能假设没有缓存访问。
  • 这不是 atomic 所能提供的:它为您提供一致的值(这意味着您将始终读取实际设置的值,而不是瞬态值)但这并不意味着最后一个值...比赛仍然存在,但至少价值观是好的
  • @OznOg 我想我明白了,但在什么情况下我们会得到一个陈旧的值,而不是最新的值?
  • 不同步,线程可以随机顺序运行,因此一个可能会长时间休眠而另一个永远不会停止运行;您可以在一个线程中添加sleep() 来模拟这一点(这是突出比赛的常用方法)

标签: c++ multithreading atomic volatile


【解决方案1】:

当我们谈论现代架构上的内存访问时,我们通常会忽略读取值的“确切位置”。

读取操作可以从缓存 (L0/L1/...)、RAM 甚至硬盘驱动器中获取数据(例如,在交换内存时)。

这些关键字告诉编译器在访问数据时使用哪些汇编操作。

易失性

告诉编译器总是从内存中读取变量的值,而不是从寄存器中读取的关键字。

这个“内存”仍然可以是缓存,但是,如果缓存中的这个“地址”被认为是“脏”的,意味着值已经被不同的处理器改变了,这个值将被重新加载。

这确保我们永远不会读取过时的值。

但是,如果类型声明 volatile 不是原始类型,其读/写操作本质上是原子的(就读/写它的汇编指令而言),我们可能会读取一个中间值(作者管理到读者阅读时只写入一半字节)。

原子

编译器看到load(读取)操作,它基本上和volatile值做的事情完全一样,除了使用原子操作(这意味着我们永远不会读取中间值)。

那么,有什么区别???

不同之处在于跨 CPU 的写操作。 使用 volatile 变量时,如果 CPU 1 设置了该值,而 CPU 2 读取它,则读取器可能会读取旧值。

但是,怎么可能呢? volatile 关键字保证我们不会读取过时的值!

嗯,那是因为作者没有公布价值!虽然读者试图阅读它,但它会阅读旧的。

当编译器偶然发现一个原子变量的store(写)操作时:

  • 在内存中自动设置值
  • 宣布值已更改

在公告之后,所有的 CPU 都会知道他们应该重新读取变量的值,因为他们的缓存将被标记为“脏”。

这种机制与对文件执行的操作非常相似。当您的应用程序写入硬盘驱动器上的文件时,其他应用程序可能会或可能不会看到新信息,具体取决于您的应用程序是否将数据刷新到硬盘驱动器。

如果数据没有被刷新,那么它只是驻留在您的应用程序缓存中的某个位置并且仅对其自身可见。刷新后,任何打开文件的人都会看到新状态。

【讨论】:

  • "writer didn't publish the value" 这是否意味着 writer 已经修改了值,但没有将值写入变量(如读取-修改-写入步骤)?所以读者会看到旧值,因为作者还没有完成write 步骤?
  • 是的,作者会看到修改后的值,但其他人不会。变量对于程序员来说是语法糖,所以实际上你不能说该值“没有将值写入变量”,而应该说“没有将新值发布到其他 CPU”。这就像用于硬盘操作的flush() 命令。当您将某些内容写入文件时,其他人看不到它,因为您没有将其刷新到硬盘上,相反,它位于您应用缓存中的某个位置,并且仅对自己可见。
  • 但是,如果编写器不能在单个指令中将值写入变量怎么办?如果有两个线程,在这种情况下会有什么行为:第一个线程将值写入变量,第二个线程读取该变量的值?这两个操作都是原子的。所以我猜编译器会使用 cas-loops:如果第二个线程不能原子读取值(因为有线程写入值),它会稍后在循环中尝试,因为原子读取保证不会返回中途值调用 @ 987654327@ 功能。或者也许第一个线程稍后会尝试写入值,允许第二个线程读取值?
  • 当CPU执行原子操作时,始终保证write+publish是原子执行的。
  • 我的意思是,latest 并不容易理解。如果 reader 已先完成读取操作,则它是 moment of reading 的最新值,即使 writer 稍后写入新值。因此,返回到第一个线程的值将是相对于第一个线程的最新值,但它不会是第二个写入线程的最新值。我说的对吗?
【解决方案2】:

如果我们声明原子变量,我们总是得到最新的值吗 调用 load() 操作的变量?

是的,对于最新的一些定义。

并发的问题是不可能以通常的方式争论事件的顺序。这来自于硬件的一个基本限制,即在多个内核上建立全局操作顺序的唯一方法是将它们序列化(并在此过程中消除并行计算的所有性能优势)。

现代处理器提供的是一种选择加入机制,用于在某些操作之间重新建立顺序。原子是该机制的语言级抽象。想象一个场景,其中两个atomic<int>s ab 在线程之间共享(我们进一步假设它们被初始化为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,一个线程可能会观察到对不同原子变量的写入发生的顺序与另一个线程观察到的顺序不同

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-10
    • 2021-06-13
    • 1970-01-01
    • 1970-01-01
    • 2015-01-25
    相关资源
    最近更新 更多