分享一个代码优化导致的死循环

1 月 4 日
 echoechoin

现象: gdb bt 卡在 spinlock_unlock() 位置。

  1. 查了半天代码,我的每一个锁都没有问题。给我看吐血了。

  2. 最后没辙了灵机一动 gdb disassemble 发现这里死循环了:

原来是一个标志位没有设置为 volatile ,代码被优化了。大概的代码如下:

while (true) {
    // 多线程中可以被修改的一个标志
    if (flags_1 == STATUS_1) { 
        continue
    }
    // 其他逻辑
    // ... ...
}

但是为什么不是必现的 BUG 呢? AI 给了一堆解释,但是没怎么看懂。

6726 次点击
所在节点    C
31 条回复
nuk
1 月 5 日
变量没有 extern ,直接传递的地址吧? gcc 检查了语法范围内没有修改的可能,直接给你优化成固定值了。用 malloc 分配 flag ,然后用指针访问就行,单读单写的情况,乱序执行和指令重排根本不会有任何 race condition 的,可以放心使用 spinlock 。
passive
1 月 5 日
信息量太少,估计你这都不一定是 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.
chinuno
1 月 5 日
这个我以前也遇到过。特定版本 GCC 编译就会出现,换个编译器就正常。在循环里改了 flag 的值也会被优化掉,最后丢 ida 看了才发现是被编译器优化了
kaivbv
1 月 5 日
这应该不是 root cause 把?
qieqie
1 月 5 日
这种情况用 volatile 也是错的,volatile 不保证内存序和原子性,在某些约束下(例如 x86, 单变量无数据依赖简单读写)碰巧能跑罢了。
Vaspike
1 月 5 日
八股文之王, 出列:
1. volatile 能保证一个线程 A 修改了被修饰的变量 x 后, 线程 B 内使用的会是修改后的 x 变量的值,提供多线程访问可见性
2. volatile 能防止指令重排序
detached
1 月 5 日
这就需要传教 Rust 了 :)

这里的核心问题是:编译器在做“单线程视角”的激进优化。

主线程中有一个死循环,编译器在分析上下文时发现:“在这个循环内部,没有任何指令修改了 flags_1”。既然没人改,那它就是个常量,于是编译器直接把逻辑优化成了死循环指令( jmp ),不再去内存里读值。

问题的关键在于:如何打破编译器的“单线程假设”,明确告诉它“这个变量会被其他线程修改”?

在 C 语言中:volatile 是这一场景下的补丁。它强制编译器对该变量的每次访问都必须从内存读取,禁止将其缓存到寄存器或优化掉。这确实间接实现了“告诉编译器别乱优化”的目的。

在 Rust 中: 这种 Bug 甚至无法通过编译。因为这个标识属于 Cross-thread shared state (跨线程共享状态)。Rust 的编译器(借用检查器)会强制要求你必须使用线程安全的包装类型——比如 Mutex<T> 或者 AtomicBool 。

一旦你用了这些类型,就等于在类型系统层面告诉了编译器:“这里涉及多线程并发”。编译器和底层硬件就会自动处理好优化屏障,完全不需要程序员去操心“要不要加 volatile”这种容易遗漏的细节。
mmdsun
1 月 5 日
今天又刷到,可以看下 Linux 内核文档关于并发的内存屏障的那块。
编译器只对语言规则负责,并发语义必须由程序员明确表达。C 语言并发模型是你不显式声明并发语义,编译器就按单线程世界优化给你看。不要让编译器猜测。这种一律按 BUG 处理。

编译器也是按标准优化,这锅甩不到编译器上面。。
这是一个多线程 bug ,bug 正是通过编译器的“合法特性”表现出来的。两者并不矛盾。
franklinyu
1 月 5 日
我记得不推荐用 volatile 做多线程同步,推荐用同步原语(比如原子变量或互斥锁)
dode
1 月 6 日
这不止是死循环,CPU 占用还是 100%,好排查一点
echoechoin
1 月 6 日
@dode 代码默认就 100% cpu 占用😂

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://study.congcong.us/t/1182913

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX