You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程链表访问场景:仅用_Atomic替代volatile是否足够?

多线程环境下_Atomic替代volatile的可行性问题

我开发了一个多线程应用,多个线程会访问同一个指针变量。用volatile不是为了保证原子访问(本来就不存在原子访问),而是防止编译器做激进优化——编译器默认认为一个线程不会影响另一个线程,会判定变量值不会改变。

我见过不少说“volatile在多线程应用中无用”的帖子。现在我有个被两个线程访问的链表,操作逻辑如下:

  • 线程1:
    • 为结构体分配内存
    • 将节点加入链表
    • 处理链表,等待某个值被设为xyz
  • 线程2:
    • 读取链表
    • 执行处理操作
    • 将值设为xyz

链表的所有操作都用FreeRTOS互斥锁保护,但这没法保证线程2能看到线程1持有的list_starts指针的最新值。

我认为代码里有两个关键问题:

  • 各线程对list_starts指针的可见性
  • 指针指向的e->data值的线程可见性

存在问题的参考代码(非生产环境)

#include <stdint.h>
#include <stdlib.h>
#include <stdatomic.h>

typedef struct mytype {
    struct mytype* next;
    int data;
} mytype_t;
typedef _Atomic mytype_t mytype_atomic_t;

mytype_t* list_starts;
//mytype_atomic_t* list_starts;

void
thread1() {
    //Start thread2 here...
    //start_thread(thread2);

    //Run the thread here...
    while (1) {
        //mutex_lock();
        /* Create the entry */
        mytype_t* entry = malloc(sizeof(*entry));
        if (entry != NULL) {
            entry->next = list_starts;
            list_starts = entry;
        }
        //mutex_unlock();
        
        //mutex_lock();
start_over:
        for (mytype_t* e = list_starts, *prev = NULL; e != NULL; prev = e, e = e->next) {
            //mutex_unlock();
            //Simply wait to become 3
            while (e->data != 3) {}
            //mutex_lock();

            //Remove from the list
            if (prev == NULL) {
                list_starts = e->next;
            } else {
                prev->next = e->next;
            }
            free(e);
            goto start_over;
        }
        //mutex_unlock();
    }
}

原代码编译后的汇编(编译参数:-mcpu=cortex-m4 -O2)

thread1:
        push    {r4, lr}
        ldr     r4, .L12
.L4:
        movs    r0, #8
        bl      malloc
        cbz     r0, .L2
        ldr     r3, [r4]
        str     r3, [r0]
.L6:
        //Critical part - loaded once
        ldr     r3, [r0, #4]
.L5:
        //Comparison and BNE between self -> no reload
        cmp     r3, #3
        bne     .L5
        ldr     r3, [r0]
        str     r3, [r4]
        bl      free
        ldr     r0, [r4]
        cmp     r0, #0
        bne     .L6
        b       .L4
.L2:
        ldr     r0, [r4]
        cmp     r0, #0
        beq     .L4
        b       .L6
.L12:
        .word   .LANCHOR0
list_starts:

修改后的代码片段

把原代码的for循环改为使用mytype_atomic_t*类型:

for (mytype_atomic_t* e = list_starts, *prev = NULL; e != NULL; prev = e, e = e->next) {
    while (e->data != 3) {}
}

修改后编译的汇编

thread1:
        push    {r3, r4, r5, lr}
        ldr     r4, .L13
        ldr     r5, [r4]
.L4:
        movs    r0, #8
        bl      malloc
        cbz     r0, .L2
        str     r5, [r0]
        str     r0, [r4]
.L6:
        adds    r2, r0, #4
.L5:
        //Barrier
        dmb     ish
        //Load first...
        ldr     r3, [r2]
        dmb     ish
        cmp     r3, #3
        //Jump back to pre-load
        bne     .L5
        dmb     ish
        ldr     r3, [r0]
        dmb     ish
        str     r3, [r4]
        bl      free
        ldr     r0, [r4]
        cmp     r0, #0
        bne     .L6
.L7:
        movs    r5, #0
        b       .L4
.L2:
        cmp     r5, #0
        beq     .L7
        mov     r0, r5
        b       .L6
.L13:
        .word   .LANCHOR0
list_starts:

核心问题

仅通过以下方式是否足够保证线程可见性?

  • 使用_Atomic关键字声明自定义类型
  • 完全不使用volatile

回答

完全足够,甚至这才是C标准规定的多线程环境下保证内存可见性的正确方式:

  1. _Atomic的本质:C11引入的_Atomic关键字不仅保证操作的原子性,更关键的是它会自动生成内存屏障(比如你看到的dmb ish指令),强制编译器和处理器不进行指令重排,同时确保变量的读写直接操作内存而非寄存器缓存,从根源上解决了线程间的内存可见性问题——这正是你用volatile想要达到的“防止编译器优化”的核心目的,但_Atomic是标准层面的解决方案,比volatile更可靠。
  2. 为什么volatile不适合多线程:volatile的设计初衷是处理内存映射IO这类硬件交互场景,它只保证变量的读写不被编译器优化,但不提供内存屏障,也不保证处理器层面的指令顺序和缓存一致性,在多线程环境下无法保证线程间的可见性,这也是很多帖子说它无用的原因。
  3. 你的场景验证:从汇编代码就能看到,使用_Atomic后,编译器在每次读取e->data时都插入了内存屏障,并且每次都会重新从内存加载值,而不是像原代码那样把值缓存到寄存器里死循环对比,完美解决了可见性问题。另外,你的链表操作已经用互斥锁保护,互斥锁本身也会隐含内存屏障,配合_Atomic对指针和数据的可见性保证,整个逻辑的线程安全性是完整的。

内容的提问来源于Stack Exchange,提问作者unalignedmemoryaccess

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 18:35:00