V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
echoechoin
V2EX  ›  C

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

  •  1
     
  •   echoechoin · 2 天前 · 5347 次点击

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

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

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

    gdb disassemble

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

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

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

    31 条回复    2026-01-06 12:28:21 +08:00
    anytk
        1
    anytk  
       2 天前
    跨线程的话,一般推荐用原子变量来做标志,内存序可以用 relaxed.

    volatile 基本留给硬件寄存器读写和 sig_atomic_t 了.
    echoechoin
        2
    echoechoin  
    OP
       2 天前
    @anytk 想来也是不应该写这种死循环,AI 建议使用 refcnt 可以多个线程在 STATUS_1 状态下继续后续的代码,但是我也懒得改了。。。
    aviator
        3
    aviator  
       2 天前
    volatile 提供了多线程访问的可见性,是得加
    qzhai
        4
    qzhai  
       2 天前
    分享一个死循环给我?我写死循环的经验可是多得很
    PTLin
        5
    PTLin  
       2 天前
    简单来说就是你没用任何同步手段,例如原子变量,内存屏障,volatile 。你这种情况下有可能会遇到
    指令重排,例如把你这个判断的代码重排到后面。
    从缓存中读取,例如把你这个 flags 放到寄存器里,然后从寄存器里读出。
    操作拆分,例如把你这个读取操作在汇编的实现中拆分成两次内存操作。

    相关的东西你还是去了解下内存序什么的吧。
    wfg
        6
    wfg  
       2 天前 via iPhone
    你贴的代码,不管有没有 continue 不都是继续循环吗?
    ccpp132
        7
    ccpp132  
       2 天前   ❤️ 1
    c++的话 volatile 也不算对,应该用 atomic ,这点和 java 还不是一回事
    PTLin
        8
    PTLin  
       2 天前
    @PTLin 甚至在一些情况下,编译器发现你后续代码没用操作这个变量,虽然你这个变量的操作是在其他线程,但是编译器并不知道这些信息,因为你没用同步手段,编译器可能会激进的吧你这个判断删除。
    yyysuo
        9
    yyysuo  
       2 天前
    @qzhai 大家都不怎么服气的。
    mmdsun
        10
    mmdsun  
       2 天前 via iPhone
    C 语言多线程经典 BUG 了。 让 ai 写一个 c 语言多线程自旋代码吧。不用原子锁条件变量肯定爆炸
    w568w
        11
    w568w  
       2 天前
    chenyu0x00
        12
    chenyu0x00  
       2 天前   ❤️ 10
    这个不是多线程的 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 还解决了多线程情况下的内存序的问题。
    opengps
        13
    opengps  
       2 天前
    @wfg #6 我猜是类似于 plc 的设备检测逻辑,真正的逻辑在后面一段
    litchinn
        14
    litchinn  
       2 天前
    为什么要加 volatile 很好理解,cpu 有多级缓存,volatile 强制从内存中读取最近修改
    但是要问为什么不是必现的 BUG 这就太复杂了
    JIT 优化,代码是否触发了上下文切换,缓存行是否失效了触发读取主存,两个线程的 CPU 是否共享同一个缓存都可能影响
    litchinn
        15
    litchinn  
       2 天前
    @litchinn 看错节点了,没有 JIT
    cnbatch
        16
    cnbatch  
       2 天前
    多线程读写的话,能用 atomic 就尽量用,C 语言也有内置的(记得是从 C11 开始的)
    midraos
        17
    midraos  
       2 天前
    @ccpp132 #7 c/c++中 volatile 无法保证多线程下的可见性,应该是有 atomic 或使用内存屏障
    yxd19
        19
    yxd19  
       2 天前
    我之前卡在 cuda device code 的内存模型上很久。。
    araraloren
        20
    araraloren  
       2 天前
    有原子变量,为何不用?
    nuk
        21
    nuk  
       2 天前
    变量没有 extern ,直接传递的地址吧? gcc 检查了语法范围内没有修改的可能,直接给你优化成固定值了。用 malloc 分配 flag ,然后用指针访问就行,单读单写的情况,乱序执行和指令重排根本不会有任何 race condition 的,可以放心使用 spinlock 。
    passive
        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.
    chinuno
        23
    chinuno  
       1 天前 via Android   ❤️ 1
    这个我以前也遇到过。特定版本 GCC 编译就会出现,换个编译器就正常。在循环里改了 flag 的值也会被优化掉,最后丢 ida 看了才发现是被编译器优化了
    kaivbv
        24
    kaivbv  
       1 天前
    这应该不是 root cause 把?
    qieqie
        25
    qieqie  
       1 天前
    这种情况用 volatile 也是错的,volatile 不保证内存序和原子性,在某些约束下(例如 x86, 单变量无数据依赖简单读写)碰巧能跑罢了。
    Vaspike
        26
    Vaspike  
       1 天前
    八股文之王, 出列:
    1. volatile 能保证一个线程 A 修改了被修饰的变量 x 后, 线程 B 内使用的会是修改后的 x 变量的值,提供多线程访问可见性
    2. volatile 能防止指令重排序
    detached
        27
    detached  
       1 天前
    这就需要传教 Rust 了 :)

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

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

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

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

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

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

    编译器也是按标准优化,这锅甩不到编译器上面。。
    这是一个多线程 bug ,bug 正是通过编译器的“合法特性”表现出来的。两者并不矛盾。
    franklinyu
        29
    franklinyu  
       1 天前 via iPhone
    我记得不推荐用 volatile 做多线程同步,推荐用同步原语(比如原子变量或互斥锁)
    dode
        30
    dode  
       19 小时 6 分钟前
    这不止是死循环,CPU 占用还是 100%,好排查一点
    echoechoin
        31
    echoechoin  
    OP
       15 小时 42 分钟前
    @dode 代码默认就 100% cpu 占用😂
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   969 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 20:10 · PVG 04:10 · LAX 12:10 · JFK 15:10
    ♥ Do have faith in what you're doing.