gdb bt 卡在 spinlock_unlock() 位置。查了半天代码,我的每一个锁都没有问题。给我看吐血了。
最后没辙了灵机一动 gdb disassemble 发现这里死循环了:

原来是一个标志位没有设置为 volatile ,代码被优化了。大概的代码如下:
while (true) {
// 多线程中可以被修改的一个标志
if (flags_1 == STATUS_1) {
continue
}
// 其他逻辑
// ... ...
}
但是为什么不是必现的 BUG 呢? AI 给了一堆解释,但是没怎么看懂。
1
anytk 2 天前
跨线程的话,一般推荐用原子变量来做标志,内存序可以用 relaxed.
volatile 基本留给硬件寄存器读写和 sig_atomic_t 了. |
2
echoechoin OP @anytk 想来也是不应该写这种死循环,AI 建议使用 refcnt 可以多个线程在 STATUS_1 状态下继续后续的代码,但是我也懒得改了。。。
|
3
aviator 2 天前
volatile 提供了多线程访问的可见性,是得加
|
4
qzhai 2 天前
分享一个死循环给我?我写死循环的经验可是多得很
|
5
PTLin 2 天前
简单来说就是你没用任何同步手段,例如原子变量,内存屏障,volatile 。你这种情况下有可能会遇到
指令重排,例如把你这个判断的代码重排到后面。 从缓存中读取,例如把你这个 flags 放到寄存器里,然后从寄存器里读出。 操作拆分,例如把你这个读取操作在汇编的实现中拆分成两次内存操作。 相关的东西你还是去了解下内存序什么的吧。 |
6
wfg 2 天前 via iPhone
你贴的代码,不管有没有 continue 不都是继续循环吗?
|
7
ccpp132 2 天前 c++的话 volatile 也不算对,应该用 atomic ,这点和 java 还不是一回事
|
8
PTLin 2 天前
@PTLin 甚至在一些情况下,编译器发现你后续代码没用操作这个变量,虽然你这个变量的操作是在其他线程,但是编译器并不知道这些信息,因为你没用同步手段,编译器可能会激进的吧你这个判断删除。
|
10
mmdsun 2 天前 via iPhone
C 语言多线程经典 BUG 了。 让 ai 写一个 c 语言多线程自旋代码吧。不用原子锁条件变量肯定爆炸
|
11
w568w 2 天前
|
12
chenyu0x00 2 天前 这个不是多线程的 bug ,这个是 C/C++编译器的"特性"。
相当于有这样一段代码: ```c++ int flag; while (flag == 0) { } ``` 编译器在不进行优化(-O0)的情况下,每次 while 循环都会从内存中读 flag 的值,并和 0 进行判断。 当使用了编译器优化的情况下,编译器就会发现"这个程序员真 SB ,为什么每次都要从内存中读 flag 的值呢,我直接读一次,放到寄存器里面,之后访问速度就更快了,判断也可以优化成一次",于是代码就变成这样了: ```c++ register eax = flag; if (eax == 0) { while (true) { } } ``` 你会发现这个优化在单线程的情况下是完全没有问题的,在多线程情况下就出问题了。 而使用 volatile int flag 就是告诉编译器"不要偷懒,我让你从内存读 flag 你就从内存读,让你写到内存就写到内存"。 不过如上面所说,建议多线程通信使用 atomic ,因为 atomic 还解决了多线程情况下的内存序的问题。 |
14
litchinn 2 天前
为什么要加 volatile 很好理解,cpu 有多级缓存,volatile 强制从内存中读取最近修改
但是要问为什么不是必现的 BUG 这就太复杂了 JIT 优化,代码是否触发了上下文切换,缓存行是否失效了触发读取主存,两个线程的 CPU 是否共享同一个缓存都可能影响 |
16
cnbatch 2 天前
多线程读写的话,能用 atomic 就尽量用,C 语言也有内置的(记得是从 C11 开始的)
|
19
yxd19 2 天前
我之前卡在 cuda device code 的内存模型上很久。。
|
20
araraloren 2 天前
有原子变量,为何不用?
|
21
nuk 2 天前
变量没有 extern ,直接传递的地址吧? gcc 检查了语法范围内没有修改的可能,直接给你优化成固定值了。用 malloc 分配 flag ,然后用指针访问就行,单读单写的情况,乱序执行和指令重排根本不会有任何 race condition 的,可以放心使用 spinlock 。
|
22
passive 2 天前 via Android
信息量太少,估计你这都不一定是 volatile 的问题。记得在哪里看到过类似的 warning ,找了一下原文:
One notable exception is Visual Studio, where, with default settings, every volatile write has release semantics and every volatile read has acquire semantics (Microsoft Docs), and thus volatiles may be used for inter-thread synchronization. Standard volatile semantics are not applicable to multi-threaded programming, although they are sufficient for e.g. communication with a signal handler that runs in the same thread when applied to sig_atomic_t variables. The compiler option /volatile:iso can be used to restore behavior consistent with the standard, which is the default setting when the target platform is ARM. |
23
chinuno 1 天前 via Android 这个我以前也遇到过。特定版本 GCC 编译就会出现,换个编译器就正常。在循环里改了 flag 的值也会被优化掉,最后丢 ida 看了才发现是被编译器优化了
|
24
kaivbv 1 天前
这应该不是 root cause 把?
|
25
qieqie 1 天前
这种情况用 volatile 也是错的,volatile 不保证内存序和原子性,在某些约束下(例如 x86, 单变量无数据依赖简单读写)碰巧能跑罢了。
|
26
Vaspike 1 天前
八股文之王, 出列:
1. volatile 能保证一个线程 A 修改了被修饰的变量 x 后, 线程 B 内使用的会是修改后的 x 变量的值,提供多线程访问可见性 2. volatile 能防止指令重排序 |
27
detached 1 天前
这就需要传教 Rust 了 :)
这里的核心问题是:编译器在做“单线程视角”的激进优化。 主线程中有一个死循环,编译器在分析上下文时发现:“在这个循环内部,没有任何指令修改了 flags_1”。既然没人改,那它就是个常量,于是编译器直接把逻辑优化成了死循环指令( jmp ),不再去内存里读值。 问题的关键在于:如何打破编译器的“单线程假设”,明确告诉它“这个变量会被其他线程修改”? 在 C 语言中:volatile 是这一场景下的补丁。它强制编译器对该变量的每次访问都必须从内存读取,禁止将其缓存到寄存器或优化掉。这确实间接实现了“告诉编译器别乱优化”的目的。 在 Rust 中: 这种 Bug 甚至无法通过编译。因为这个标识属于 Cross-thread shared state (跨线程共享状态)。Rust 的编译器(借用检查器)会强制要求你必须使用线程安全的包装类型——比如 Mutex<T> 或者 AtomicBool 。 一旦你用了这些类型,就等于在类型系统层面告诉了编译器:“这里涉及多线程并发”。编译器和底层硬件就会自动处理好优化屏障,完全不需要程序员去操心“要不要加 volatile”这种容易遗漏的细节。 |
28
mmdsun 1 天前 via iPhone
今天又刷到,可以看下 Linux 内核文档关于并发的内存屏障的那块。
编译器只对语言规则负责,并发语义必须由程序员明确表达。C 语言并发模型是你不显式声明并发语义,编译器就按单线程世界优化给你看。不要让编译器猜测。这种一律按 BUG 处理。 编译器也是按标准优化,这锅甩不到编译器上面。。 这是一个多线程 bug ,bug 正是通过编译器的“合法特性”表现出来的。两者并不矛盾。 |
29
franklinyu 1 天前 via iPhone
我记得不推荐用 volatile 做多线程同步,推荐用同步原语(比如原子变量或互斥锁)
|
30
dode 19 小时 6 分钟前
这不止是死循环,CPU 占用还是 100%,好排查一点
|
31
echoechoin OP @dode 代码默认就 100% cpu 占用😂
|