嵌入式C99:如何解决多核CPU上的并发问题
多线程/多核场景下的C语言Data Race问题与解决方案
背景:C标准中的Data Race定义
C11标准(ISO/IEC 9899)新增了第5.1.2.4章《多线程执行与数据竞争》,并引入<stdatomic.h>头文件规范原子操作。
C2024标准(ISO/IEC 9899:2024)第5.1.2.5节第35段明确:
若程序执行中包含两个来自不同线程的冲突操作,且至少其中一个操作不是原子操作,且两者无先行关系,则存在data race。此类data race会导致未定义行为。
存在Data Race的示例代码
当生产者和消费者在不同执行线程中运行时,以下代码因无同步的共享变量访问会引发未定义行为:
Data data; unsigned int ready = 0; /* 0 表示未就绪;非0表示就绪 */ void producer(void) /* 线程1调用 */ { initializeData(); ready = 1; } void consumer(void) /* 线程2调用 */ { while(ready == 0) /* Data Race 发生处 */ { /* 等待就绪 */ } useData(); }
C11标准下的修复方案
将共享变量ready声明为原子类型,借助顺序一致性(sequential consistency)保证内存操作的可见性和顺序,修复未定义行为:
Data data; atomic_uint ready = 0; /* 0 表示未就绪;非0表示就绪 */ void producer(void) /* 线程1调用 */ { initializeData(); ready = 1; /* 注:原示例此处为`ready = 0`,属于逻辑笔误,修正为符合业务语义的赋值 */ } void consumer(void) /* 线程2调用 */ { while(ready == 0) { /* 等待就绪 */ } useData(); }
C99标准未提及多线程或data race相关内容,但跨执行流对共享变量的无同步冲突访问仍会引发data race和未定义行为。
技术问题解答
1. 仅支持C99的嵌入式平台如何修复此类Data Race?
由于C99标准本身不提供多线程同步机制,需依赖编译器扩展和硬件特性:
- 编译器内置原子操作:使用编译器提供的原子原语,比如GCC的
__sync_bool_compare_and_swap、__sync_fetch_and_add,或ARM编译器的__ldrex/__strex,保证对共享变量的读写操作是原子的。 - 内存屏障指令:插入编译器层面的内存屏障(如GCC的
__asm__ __volatile__ ("" ::: "memory"))阻止指令重排,同时根据硬件架构插入对应硬件内存屏障(如ARM的dmb、x86的mfence),确保内存操作的跨核心可见性。 - 硬件级互斥锁:基于平台硬件指令实现自旋锁,用互斥锁包裹所有对共享变量的访问,保证同一时间只有一个执行流能操作共享数据。
2. 多核无线程场景下的并发访问是否会引发未定义行为?
是的,这种场景等同于不同执行线程的并发访问,会引发未定义行为。
不管是操作系统调度的线程,还是多核上直接运行的独立任务,只要是不同执行流对共享变量进行冲突操作(一个写操作 + 一个读/写操作),且没有同步机制,就符合C标准中data race的定义。C99标准的抽象机默认是单执行流,超出该范畴的并发访问本身属于标准未定义行为,而data race会进一步导致不可预测的程序行为。
3. 多核场景下是否存在更简便的解决方案?
有几种更简便的思路:
- 利用硬件同步机制:很多嵌入式多核芯片提供核心间邮箱、全局信号量或中断控制等硬件同步原语,直接使用这些机制实现核心间同步,比纯软件实现更可靠高效。
- 编译器扩展原子类型:即使不支持C11的
<stdatomic.h>,多数主流编译器(如GCC、Clang)在C99模式下也支持_Atomic关键字扩展,直接声明共享变量为原子类型,编译器会自动生成对应的原子操作和内存屏障。 - 避免共享状态:如果业务逻辑允许,将数据拆分到各核心的本地私有内存(如CPU的私有缓存区域),通过消息传递的方式交换数据,从根源上消除data race的可能。
内容的提问来源于stack exchange,提问作者Sonic78




