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

