如果同一处理器中有两个线程,则可能会出现读/写撕裂。
例如,在 32 位系统上,线程 1 和线程 2 运行在同一内核上:
- 线程 1 将 64 位 int 0xffffffffffffffff 分配给全局变量 X,该变量最初为零。
- 前32位设置为前32位设置在X中,现在X为0xffffffff00000000
- 线程 2 将 X 读取为 0xffffffff00000000
- 线程 1 写入最后 32 位。
撕裂的读取发生在第 3 步。
但是如果满足以下条件怎么办:
- 线程 1 和线程 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就是这样。)
进一步阅读:
- Atomicity on x86 - 单个加载或存储究竟如何是原子的
- Why is integer assignment on a naturally aligned variable atomic on x86?
- Can num++ be atomic for 'int num'? - 原子 RMW 如何与 MESI 交互。 (对于 x86 风格的单指令,例如
lock add [mem]、eax
。LL/SC 机器只是检测它们在某处失去了对缓存行的控制并报告失败.)
关于multithreading - 如果系统是缓存一致的,您是否可以在固定到不同处理器的两个线程之间进行撕裂读/写?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64602829/