multithreading - 如果系统是缓存一致的,您是否可以在固定到不同处理器的两个线程之间进行撕裂读/写?

标签 multithreading cpu-architecture cpu-cache atomic

如果同一处理器中有两个线程,则可能会出现读/写撕裂。

例如,在 32 位系统上,线程 1 和线程 2 运行在同一内核上:

  1. 线程 1 将 64 位 int 0xffffffffffffffff 分配给全局变量 X,该变量最初为零。
  2. 前32位设置为前32位设置在X中,现在X为0xffffffff00000000
  3. 线程 2 将 X 读取为 0xffffffff00000000
  4. 线程 1 写入最后 32 位。

撕裂的读取发生在第 3 步。

但是如果满足以下条件怎么办:

  1. 线程 1 和线程 2 固定到不同的内核
  2. 系统使用MESI协议(protocol)实现缓存一致性

这种情况下,撕读还有可能吗?或者缓存行会在步骤 3 中被视为无效,从而防止撕裂读取?

最佳答案

是的,你可能会流泪。

对该行的共享请求可能会出现在提交两个单独的 32 位存储之间。如果它们是通过单独的指令完成的,则写入线程甚至可能在第一个和第二个存储之间进行中断,从而阻止存储缓冲区( into aligned 64-bit commits like some 32-bit RISC CPUs are documented to do )中的任何存储合并,这通常可能导致在实践中很难观察到两个存储之间的撕裂。独立的 32 位存储。

另一种导致撕裂的方法是,读取端在读取前半部分之后、读取后半部分之前失去对缓存行的访问权限。 (因为它从写入器核心接收到 RFO(读取所有权)。)第一次读取可以看到旧值,第二次读取可以看到新值。

唯一安全的方法是存储和加载都作为对相应核心的 L1d 缓存的单个原子访问来完成。

(如果互连本身不会引入撕裂;请注意 AMD K10 Opteron that tears on 8-byte boundaries between cores on separate sockets 的情况,但同一套接字中的内核之间似乎具有对齐的 16 字节原子性。x86 手册仅保证 8 字节原子性,因此 16 字节原子性超出了记录的保证,这是实现的副作用。)

当然,一些 32 位 ISA 具有加载对或存储对指令,或者(如 x86)通过 FPU/SIMD 单元完成的 64 位对齐加载/存储保证原子性。


如果撕裂通常是可能的,那么这样的微架构将如何实现 64 位原子操作?

通过延迟对 MESI 请求的响应,在执行一对加载或一对存储时共享或使一行无效,该指令使用特殊指令完成,当正常加载对或存储对不会时,该指令提供原子性t。另一个核心陷入等待响应的状态,因此必须严格限制延迟响应的时间,否则就会出现饥饿/整体吞吐量低的问题。

通常对加载对/存储对的缓存进行 64 位访问的微体系结构可以通过将一个缓存访问拆分为两个寄存器输出来免费获得原子性。

但是低端实现可能没有如此宽的缓存访问硬件。也许只有LL/SC特殊指令具有 2 寄存器原子性。 (IIRC,有些版本的ARM就是这样。)

进一步阅读:

关于multithreading - 如果系统是缓存一致的,您是否可以在固定到不同处理器的两个线程之间进行撕裂读/写?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64602829/

相关文章:

C : request for member ‘____’ in something not a structure or union 中的编译错误

c - 测试缓存失效和刷新

cpu - 如何使用(读/写)CPU 缓存 L1、L2、L3

rust - 无法在 Rust 中重现错误的缓存行共享问题

C# 套接字 : Do I really need so many separate threads

java - 新的SingleThreadscheduledExecutor

java - "subsequent read"在 volatile 变量的上下文中意味着什么?

caching - 什么是引用地点?

c++ - 关键部分或互斥体应该是真正的成员变量还是什么时候应该是?

java - 为什么子线程中的synchronized方法会持有主线程的锁