对 std::atomic 变量的更改 (read/writes) 如何跨线程传播
How do changes (read/writes) to std::atomic variables propagate across threads
我最近问过这个问题do-i-need-to-use-memory-barriers-to-protect-a-shared-resource
对于这个问题,我得到了一个非常有趣的答案,它使用了这个假设:
Changes to std::atomic variables are guaranteed to propagate across threads.
为什么会这样?它是如何完成的?这种行为如何符合 MESI 协议?
它们实际上不必传播,缓存一致性模型(MESI 或更高级的东西)为您提供内存行为一致的保证,几乎就像它是平坦的并且不存在缓存副本一样。顺序一致性增加了系统中所有代理对相同观察顺序的保证(注意 - 大多数 CPUs 不单独通过 HW 提供顺序一致性)。
如果一个线程执行内存写入(甚至不是原子的),它运行的核心将获取该行并获得对其的所有权。一旦写入完成,任何试图观察该行的线程都保证看到更新的值,即使该行仍然驻留在修改核心中——通常这是通过侦听核心并从中获取该行作为响应来实现的.高速缓存一致性协议将保证,如果这样的修改存在于某个核心本地 - 寻找该行的任何其他核心最终一定会看到它。为此,CPU 我使用窥探过滤器、目录管理(通常用于跨套接字一致性)或其他方法。
现在,您要问为什么原子很重要?有两个原因。首先 - 以上所有内容仅适用于变量驻留在内存中而不是寄存器中的情况。这是编译器的决定,所以正确的类型告诉它这样做。其他范例(如 open-MP 或 POSIX 线程)有其他方式告诉编译器变量需要通过内存共享。
其次 - 现代内核乱序执行操作,我们不希望任何其他操作传递该写入并暴露陈旧数据。 std::atomic 告诉编译器执行最强的内存排序(通过使用显式防护或锁定 - 检查生成的汇编代码),这意味着所有线程的所有内存操作将具有相同的全局排序。如果您不这样做,可能会发生奇怪的事情,例如核心 A 和核心 B 对同一位置的 2 次写入的顺序不一致(这意味着它们可能会在其中看到不同的最终值)。
最后当然是实际的原子性——如果你的数据类型不是保证原子性的,或者它没有正确对齐——这也将为你解决这个问题(否则一致性问题会加剧——想想某些线程试图更改 2 个缓存行之间的值拆分,并且不同的核心看到部分值)
我最近问过这个问题do-i-need-to-use-memory-barriers-to-protect-a-shared-resource
对于这个问题,我得到了一个非常有趣的答案,它使用了这个假设:
Changes to std::atomic variables are guaranteed to propagate across threads.
为什么会这样?它是如何完成的?这种行为如何符合 MESI 协议?
它们实际上不必传播,缓存一致性模型(MESI 或更高级的东西)为您提供内存行为一致的保证,几乎就像它是平坦的并且不存在缓存副本一样。顺序一致性增加了系统中所有代理对相同观察顺序的保证(注意 - 大多数 CPUs 不单独通过 HW 提供顺序一致性)。
如果一个线程执行内存写入(甚至不是原子的),它运行的核心将获取该行并获得对其的所有权。一旦写入完成,任何试图观察该行的线程都保证看到更新的值,即使该行仍然驻留在修改核心中——通常这是通过侦听核心并从中获取该行作为响应来实现的.高速缓存一致性协议将保证,如果这样的修改存在于某个核心本地 - 寻找该行的任何其他核心最终一定会看到它。为此,CPU 我使用窥探过滤器、目录管理(通常用于跨套接字一致性)或其他方法。
现在,您要问为什么原子很重要?有两个原因。首先 - 以上所有内容仅适用于变量驻留在内存中而不是寄存器中的情况。这是编译器的决定,所以正确的类型告诉它这样做。其他范例(如 open-MP 或 POSIX 线程)有其他方式告诉编译器变量需要通过内存共享。 其次 - 现代内核乱序执行操作,我们不希望任何其他操作传递该写入并暴露陈旧数据。 std::atomic 告诉编译器执行最强的内存排序(通过使用显式防护或锁定 - 检查生成的汇编代码),这意味着所有线程的所有内存操作将具有相同的全局排序。如果您不这样做,可能会发生奇怪的事情,例如核心 A 和核心 B 对同一位置的 2 次写入的顺序不一致(这意味着它们可能会在其中看到不同的最终值)。
最后当然是实际的原子性——如果你的数据类型不是保证原子性的,或者它没有正确对齐——这也将为你解决这个问题(否则一致性问题会加剧——想想某些线程试图更改 2 个缓存行之间的值拆分,并且不同的核心看到部分值)