- 主题:struct 内成员变量多线程可见性的问题
有一个问题忽然有点糊涂,发上来请教一下。
我有一个 concurrent queue,生产者把消息的值设置好,enqueue,消费者 dequeue,然后处理。处理完消息被回收,重复使用。我理解 concurrent queue 里面的原子操作负责保证指令不会乱序,即:消息置值的指令领先于进队的指令,处理消息的指令落后于出队的指令。我想到还有一个可见性的问题:怎么保证消费者在访问消息struct里变量时,看到的是最新的值,而不会因为线程用了本地缓存看到过期的值?我能想到的是把消息里的变量变为 std::atomic,利用 store, load 保证不同核之间同步。如果有变量大于 64 位,则只能依赖 mutex?
但又觉得有些多余,因为感觉 concurrent queue/message passing 本身就是一种同步模型了。这里消息成员变量的 std::atomic/mutex 是必须的么?
--
FROM 45.56.153.*
嗯,concurrent queue 是这样的。这两天在网上也看了一些资料,大概明白一些。
这个问题有两个方面,编译器的指令重排,和 cpu 体系结构上的保证。编译器层面,store 保证在此之前的 write 不会移到 store 后面。load 保证后面的 read 不会移到 load 前面。
在体系结构上,
(1) 所有的 write 会先进入本地的 store buffer。store buffer 会异步的方式写入内存,但是本地 core 在读 cache 时会搜索 store buffer。所以本地看到的永远是最新值。
(2) std::atomic 里的 store 命令把 store buffer 里的内容写到内存,同时通知所有 core,把其他 core 上对应 cache 做 invalidate。这个结构叫 invalidation queue。Invalidation queue 也是异步的方式更新本地 cache。core 在读 cache 时不会搜索 invalidation queue。所以如果不做特殊处理,其他 core 上看到的变量有可能落后。
(3) std::atomic 里 load 的作用就是主动应用 invalidation queue。之后就只能从内存里读,也就读到了 store 之后的最新值。
因为 store buffer 和 invalidation queue 是一体更新的,所以在 store 命令之前更新的变量,也必然出现在 store buffer 中并被写入了内存。换句话说,
x=1;
y=2;
z.store(3);
虽然只有 z 用了 store,但对于另一个线程里 z.load() 之后所有的命令都能看到最新的 x 和 y。
我之前糊涂的地方,在于没意识到 store buffer 是整体作用的。x, y 必然先进 store buffer。不存在只把 z 写回内存 (因为用了 store),而把 x, y 漏掉的情况。
【 在 lllho 的大作中提到: 】
: 你用的库应该至少有配置对入队时做写屏障,出队时做读屏障
--
修改:Maksim FROM 45.56.153.*
FROM 45.56.153.*
我这里说的 store, load 都是 std::atomic_store, std::atomic_load。在 C++ 层面,std::atomic 逻辑上对 consistency 模型做了明确得语义描述。编译器根据语义在生成指令时会根据架构选择一条或者多条指令来选择加不加 barrier,xchg 或者 mov-store 或者 mov-store + mfence。在给定语义下 (比如 sequential consistency),对应的指令就要把 store buffer 写回内存。
p.s. "store buffer 出去后进的是 cache" 我不确定,我看到的不是这样。道理上,store buffer 就是缓存内存写。而且 cpu 在读 cache 时也会查找 store buffer,写回 cache 不是多此一举吗?
【 在 BigCarrot 的大作中提到: 】
: :
: 你仍然没有搞清楚
: 从store buffer出去之后进的是cache,离内存还远着呢
: ...................
--
FROM 167.220.233.*