多线程计数结果异常原因分析
假设有两个线程,它们同时对全局变量a执行加一操作,而没有使用锁或其他同步机制来保护这个操作。
线程1读取a的值为0,计划将其加一。
同时,线程2也读取a的值为0,计划将其加一。
线程1完成加一操作,将结果1写回到它的缓存中。
线程2也完成加一操作,将结果1写回到它的缓存中。
最终,两个线程都试图将它们的结果同步回主内存,但是由于缺乏同步机制,a的实际增加只发生了一次,导致a最终的值为1而不是正确的2。
缓存一致性和同步
在多核处理器中,缓存一致性协议(如MESI)确保所有的CPU缓存对于同一内存位置的视图是一致的。这意味着,当一个核心修改了它的缓存中的a的值,这个修改必须被传播到其他核心的缓存以及最终同步回主内存。
当一个核心想要写入a时,它必须首先获取对a的独占访问权。如果其他核心也缓存了a的值,那么这些值必须被标记为无效(在MESI协议中,这对应于将缓存行的状态从Shared状态改为Invalid状态),迫使其他核心在下次访问a时重新从主内存加载。
一旦核心完成对a的修改,这个修改最终会通过一个称为“写回”(write-back)的过程被同步回主内存。这个过程的具体时机取决于缓存一致性协议和具体的缓存策略。
为避免cacheline问题。可以使用__attribute__(aligned(64))避免多核同步问题,比如
struct __attribute__((aligned(64))) SenderInfo {
size_t nsuccess;
int64_t latency_sum;
};
在x86平台,x.fetch_add(n)原子指令汇编是什么
在x86平台,执行原子加法操作(比如 x.fetch_add(n)
) 通常会用到 LOCK
前缀配合 ADD
指令或者 XADD
指令来实现。具体使用哪种指令取决于具体的上下文和编译器的选择。下面是两种实现方式的基本形式:
使用 LOCK ADD
:
LOCK ADD [memory], imm
这里,[memory]
表示需要操作的内存地址,imm
是立即数,表示要加的值。LOCK
前缀确保了这个操作是原子的,即在这条指令执行的过程中,CPU不会执行该内存地址的其他操作。
使用 LOCK XADD
:
LOCK XADD [memory], reg
这里,[memory]
同样表示目标内存地址,reg
是寄存器,表示要加的值。不同于 LOCK ADD
,LOCK XADD
将 [memory]
的原始值放入 reg
,然后将 reg
的值和 [memory]
的原始值相加,结果存回 [memory]
。这也是原子操作。
示例
假设我们有一个变量 x
存储在某个内存地址,我们想要原子地增加这个变量的值,可以用类似下面的汇编代码(以 LOCK XADD
为例):
mov eax, n ; 将n的值移动到eax寄存器
lock xadd [x], eax ; 原子地将eax的值加到x指向的内存,然后将原始的x的值存入eax
注意,这里的 n
和 x
分别代表要加的值和目标内存地址。实际汇编代码中的标识符会根据具体情况有所不同。
总结一下,虽然具体的指令编写可能因编译器的具体实现和优化策略而异,但在x86平台上实现原子加法操作通常会用到 LOCK
前缀以确保操作的原子性。
此时cache和内存数值如何变化的
使用 LOCK
前缀的原子指令,如 LOCK ADD
或 LOCK XADD
,在 x86 架构中确保了在指令执行过程中对应的内存位置是独占访问的,这意味着指令执行期间处理器会阻止其他核心或处理器对这个内存地址进行任何形式的访问。这是通过锁定这个内存地址所在的缓存行来实现的,确保操作的原子性。
在讨论缓存和内存的数值变化时,我们需要了解到 CPU 缓存(如 L1, L2, L3)和主内存(RAM)之间的交互。下面是一个高层次的概述:
-
执行前: 假设变量
x
存在于某个内存地址中。在执行原子加法操作之前,这个地址的值可能已经在一个或多个 CPU 核心的缓存中。 -
锁定缓存行: 当执行含
LOCK
前缀的指令时,CPU 会锁定包含变量x
的缓存行。这意味着任何试图访问这个缓存行的其他核心都会被阻塞直到当前操作完成。这就是如何确保操作原子性的机制。 -
执行操作: CPU 更新缓存行中变量
x
的值。如果是LOCK XADD
,它还会将原始值传递到指定的寄存器。 -
写回内存: 根据缓存一致性协议(如 MESI),修改后的缓存行最终会被写回到主内存中,这个过程可能会稍后发生,具体取决于缓存策略和系统的当前状态。
-
解锁缓存行: 一旦操作完成并且缓存行被标记为需要写回(如果有修改的话),缓存行就会解锁,其他处理器核心现在可以访问这个缓存行了。
在整个过程中,LOCK
前缀确保了即使是在多核心环境中,操作也是原子的:没有其他操作可以在原子操作的中间阶段读取或修改目标内存地址的值。这就避免了潜在的竞争条件和不一致状态。