内存模型定义一系列规范,来保证多线程访问共享变量时的 可见性、有序性 和 原子性。
内核中定义的内存屏障
以x86为例:
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) \
do \
{ \
(void)xchg(&var, value); \
} while (0)
store buffer
接前文,cache 的基本原理 和 多核心cache的一致性。
CPU设计者都是在极致压榨其性能,cache就是CPU设计者压榨CPU性能的一种体现,但是这样他们觉得还不够。
考虑一种状况,假设当core1执行一次 store 操作,且这个 store 操作的 内存地址 对应的 cacheline 在shared 状态,那么core1 需要向 core2 发送 Invalidate message,并且需要等到 core2 返回 invalidate ack 之后才能继续向下执行,那么在这个期间 core1就处于盲等阶段,那么core1必须要等这么久吗?

我们发现上面这个操作时同步的动作,基于应用开发的角度,我们往往会用队列将其优化为异步的动作。于是同理 CPU 设计者引入了 store buffer,这个 buffer 处于 CPU 与 cache 之间,但它只负责缓存 CPU的写操作。

有了 store buffer 之后 core1 如果执行 store 操作就不用立刻向 core2 发送 invalidate message 了,core1只需要将 store值 添加到 store buffer 中即可。但是引入store buffer 会带来问题。
store buffer 带来的问题
考虑下面的例子:
初始情况,假定 a 和 b 都为0,且 a 在CPU1的cache line 中,而 b 在 CPU0 的 cache line 中。
// a, b init to 0.
a = 1;
b = a + 1;
assert(b == 2);
| 步骤 | CPU# | 操作 |
|---|---|---|
| 1 | 0 | CPU0 发现 a 不在 Cache 中,便发送 Reda Invalidate 给其他的CPU |
| 2 | 0 | CPU0 将 a = 1 的值 存放至 store buffer |
| 3 | 1 | CPU1 收到 "Read Invalidate" 消息后,将a所在的cache line 移除,并返回 "Read Response" 和 "Invalidate" 消息。 |
| 4 | 0 | CPU0 执行 b = a + 1 |
| 5 | 0 | CPU0 收到步骤3消息后,将数据放入对应的 cache line,此时 cache line 的状态为 exclusive,并从cache 中载入a的值,为 a = 0 |
| 6 | 0 | CPU0 将 store buffer 中 a = 1 的信息更新到该cache line 中 |
| 7 | 0 | CPU0 用步骤5得到的 a 的值,算出b = 1,从而导致 断言。 |
- store forwarding
为了解决这一问题,增加了 store forwarding ,CPU 在载入数据时,会同时查看 cache 和 store buffer ,这样,即使store buffer 中的数据还未写到cache line,同一CPU自身的后续load 操作依旧可以使用 store buffer 中的数据。
void foo() // CPU0 执行
{
a = 1; // store
b = 1; // store
}
void bar() // CPU1 执行
{
while (b == 0) // load
continue;
asset(a == 1); // load
}
下面假定多CPU执行这段代码。初始情况仍然假定 a 和 b 都为0,且 a 在CPU1的 cache line 中,而 b 在CPU0的 cache line 中,CPU0 执行foo(),CPU1执行bar()。
| 步骤 | CPU# | 操作 |
|---|---|---|
| 1 | 0 | CPU0 执行 a=1,发现 a 不在cache中,便发送"Read Invalidate"给其他CPU,同时将 a 的值1 写入 store buffer |
| 2 | 1 | CPU1 执行 while 循环,发现 b 不在cache中,便发送"Read"消息 |
| 3 | 0 | CPU0 执行b = 1,注意(并未等待"Read Response" 和 "Invalidate"),因为 b 已在 cache (状态为"modified"或者"exclusive")中,故可以直接将值1写入 cache line |
| 4 | 0 | CPU0 收到"Read"消息,其返回"Read Response"消息,并将对应的 cache line 状态更改为"shared",注意,消息中的 b 的值已经为1 |
| 5 | 1 | CPU1 收到 "Read Response" 消息,并将消息中的 cache line 放入到自身的 cache |
| 6 | 1 | CPU1 从 cache 中载入b的值,发现为1,结束 while 循环 |
| 7 | 1 | CPU1执行assert,首先从cache 中读取a 的值,发现为0,断言fails |
| 8 | 1 | CPU1 收到"Read Invalidate",将移除 a 所在的 cache line ,同时返回 "Read Response" 和 "Invalidate Acknowledge" 消息 |
| 9 | 0 | CPU0 收到 "Read Response" 和 "Invalidate Acknowledge"消息,将 store buffer 中的 a = 1 写入到cache line |
从上面的流程来看,主要是因为第8行太迟了。
那么如何解决上述分歧呢?我们需要告诉CPU,让它们 有一致的理解。于是轮到了 内存屏障。
void foo()
{
a = 1;
smp_mb();
b = 1;
}
void bar()
{
while(b == 0)
continue;
assert(a == 1);
}
smp_mb()便是内存屏障,它后面的语句要执行写入cache line 的操作前,必须先把store buffer 的内容处理完成。
| 步骤 | CPU# | 操作 |
|---|---|---|
| 1 | 0 | CPU0 执行 a=1,发现 a 不在 cache 中,便发送"Read Invalidate" 给其他CPU,同时将 a 的值1 写入 store buffer |
| 2 | 1 | CPU1 执行 while 循环,发现 b 不在cache中,便发送"Read"消息 |
| 3 | 0 | CPU0 执行 smp_mb(),标记当前store buffer中的所有条目,目前只有a=1 |
| 4 | 0 | CPU0 执行b = 1,b 虽然已在cache 中,但发现store buffer 中已有标记的条目,证明之前的yo |
| 5 | 0 | CPU0 收到"Read"消息,其返回"Read Response"消息,并将对应的 cache line 状态更改为"shared",注意,消息中的 b 的值已经为1 |
| 6 | 1 | CPU1 收到"Read Response",并将消息中的 cache line 放入到自身cache |
| 7 | 1 | CPU1 从 cache 中载入 b 的值,发现为0,继续 while 循环 |
| 8 | 1 | CPU1 收到 消息,将移除 a 所在的cache line,同时返回 "" 和 "" 消息,注意,"" 消息中 的数据,a = 0 |
| 9 | 0 |
invalidate queue

invalidate queue 带来的问题
TSO 模型
所以我们可以这么理解,CPU0 的 store-load 操作,在别的 CPU 看来乱序执行了,变成了 load-store 次序,这种内存模型,我们称之为完全存储定序(Total Store Order),简称 TSO。
store 和 load的组合有4种:分别是 store-store,store-load,load-load 和 load-store。TSO模型中,只存在 store-load 存在乱序,另外3种内存操作不存在乱序,x86 就是TSO模型。
CPU屏障
CPU 屏障:
sfence
实现Store Barrior 会将store buffer中缓存的修改刷入L1 cache中,使得其他cpu核可以观察到这些修改,而且之后的写操作不会被调度到之前,即sfence之前的写操作一定在sfence完成且全局可见。lfence
实现Load Barrior 会将invalidate queue失效,强制读取入L1 cache中,而且lfence之后的读操作不会被调度到之前,即lfence之前的读操作一定在lfence完成(并未规定全局可见性)。mfence
实现Full Barrior 同时刷新store buffer和invalidate queue,保证了mfence前后的读写操作的顺序,同时要求mfence之后写操作结果全局可见之前,mfence之前写操作结果全局可见。lock
用来修饰当前指令操作的内存只能由当前CPU使用,若指令不操作内存仍然由用,因为这个修饰会让指令操作本身原子化,而且自带Full Barrior效果;还有指令比如IO操作的指令、exch等原子交换的指令,任何带有lock前缀的指令以及CPUID等指令都有内存屏障的作用。
C++ 11 为内存屏障提供了专门的函数``,方便统一移植。
#include <atomic>
std::atomic_thread_fence(std::memory_order_acquire);
std::atomic_thread_fence(std::memory_order_release);
代码实际例子
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
// Set either of these to 1 to prevent CPU reordering
#define USE_CPU_FENCE 1
//-------------------------------------
// Main program, as decribed in the post
//-------------------------------------
sem_t beginSema1;
sem_t beginSema2;
sem_t endSema;
int X, Y;
int r1, r2;
void *thread1Func(void *param)
{
for (;;)
{
sem_wait(&beginSema1); // Wait for signal
X = 1;
#if USE_CPU_FENCE
asm volatile("mfence" ::
: "memory"); // Prevent CPU reordering
#else
asm volatile("" ::
: "memory"); // Prevent compiler reordering
#endif
r1 = Y;
sem_post(&endSema); // Notify transaction complete
}
return NULL; // Never returns
};
void *thread2Func(void *param)
{
for (;;)
{
sem_wait(&beginSema2); // Wait for signal
Y = 1;
#if USE_CPU_FENCE
asm volatile("mfence" ::
: "memory"); // Prevent CPU reordering
#else
asm volatile("" ::
: "memory"); // Prevent compiler reordering
#endif
r2 = X;
sem_post(&endSema); // Notify transaction complete
}
return NULL; // Never returns
};
int main()
{
// Initialize the semaphores
sem_init(&beginSema1, 0, 0);
sem_init(&beginSema2, 0, 0);
sem_init(&endSema, 0, 0);
// Spawn the threads
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread1Func, NULL);
pthread_create(&thread2, NULL, thread2Func, NULL);
// Repeat the experiment ad infinitum
int detected = 0;
for (int iterations = 1;; iterations++)
{
// Reset X and Y
X = 0;
Y = 0;
r1 = 11;
r2 = 12;
// Signal both threads
sem_post(&beginSema1);
sem_post(&beginSema2);
// Wait for both threads
sem_wait(&endSema);
sem_wait(&endSema);
// Check if there was a simultaneous reorder
if (r1 == 0 && r2 == 0)
{
detected++;
printf("%d reorders detected after %d iterations\n", detected, iterations);
}
}
return 0; // Never returns
}
在不使用 cpu 屏障时,即USE_CPU_FENCE宏为0时,会出现r1 == 0 && r2 == 0 条件成立。