同步与互斥学习笔记
一、基本概念
同步与互斥是多任务/多线程编程中的两个核心机制:
同步:指多个任务之间存在明确的先后顺序,一个任务必须等待另一个任务完成某些操作后才能继续执行。
互斥:指多个任务在同一时刻争抢使用同一资源(临界资源),必须通过某种机制保证同一时间只有一个任务可以使用该资源。
二、线程安全问题的由来
问题描述
当多个任务同时访问一个共享资源(如全局变量)时,如果没有适当的保护机制,就会出现数据不一致的问题。
示例分析:全局变量 count++
c
int count = 0; // 两个线程同时执行 count++
count++
实际上包含三个步骤(非原子操作):
读取
count
的值到寄存器对寄存器中的值加 1
将结果写回
count
的内存地址
执行流程(可能出现的问题):
线程 A 读取
count = 0
,准备加 1;此时切换到线程 B,也读取
count = 0
,完成加 1 并写回,count
变为 1;切换回线程 A,继续执行加 1(基于之前读到的 0),写回后
count
仍为 1。
预期结果为 2,实际结果为 1,这就是典型的线程安全问题,也称为数据竞争。
三、同步机制的缺陷
使用同步机制(如忙等待)会导致任务死等,浪费 CPU 资源。
应避免使用
while()
循环进行无意义的等待,而应使用阻塞机制让出 CPU。
四、全局变量在 RTOS 中的存储位置
问题:全局变量存储在哪个栈中?
答案:全局变量不属于任何一个任务的栈。
内存区域划分:
代码区 (Text Segment):存放程序指令。
全局/静态数据区 (Data/BSS Segment):
存放全局变量和静态变量;
在程序启动时分配,生命周期贯穿整个程序;
被所有任务和中断共享。
堆区 (Heap):动态分配的内存(如
malloc
/pvPortMalloc
)。栈区 (Stack):
主栈/中断栈:用于 ISR 和内核调度;
任务栈:每个任务独立拥有,用于存放局部变量和上下文。
结论:
全局变量存储在全局数据区,是共享资源,访问时需使用临界区、信号量、互斥锁等机制进行保护。
五、volatile
关键字的作用
问题背景:
在编译器优化的情况下,可能会将变量缓存在寄存器中,导致多任务环境中读取到旧值。
示例代码(无 volatile
):
c
int g_calc_end = 0;// Writer 任务 void vWriterTask(void *pvParameters) {while(1) {g_calc_end = 1; // 修改全局变量} }// Reader 任务 void vReaderTask(void *pvParameters) {while(1) {if (g_calc_end == 1) { // 判断全局变量// 执行操作g_calc_end = 0;}vTaskDelay(1);} }
问题流程:
Reader 任务第一次读取
g_calc_end
到寄存器;编译器优化后,后续判断直接使用寄存器中的值(不再从内存读取);
Writer 任务修改了内存中的
g_calc_end
;Reader 任务仍然使用寄存器中的旧值,导致判断错误。
解决方案:使用 volatile
c
volatile int g_calc_end = 0;
volatile
的作用:
告诉编译器该变量是“易变的”;
禁止对其进行优化:
每次读取必须从内存中重新加载;
每次写入必须立即写回内存。
六、补充与总结
疏漏补充:
原子操作:某些架构提供原子指令(如
__atomic_inc
),可避免数据竞争;临界区保护:使用开关中断、调度器锁、互斥量等方法保护共享资源;
任务通信机制:除了全局变量,还可使用队列、事件组、信号量等进行任务间同步与通信;
内存屏障:在多核系统中,可能需要使用内存屏障指令确保内存访问顺序。
总结图示:
(见原笔记中的图片,图示展示了同步与互斥的机制和资源访问流程)
七、最佳实践建议
尽量避免使用全局变量,优先使用 RTOS 提供的通信机制;
若必须使用共享资源,务必使用互斥锁或信号量进行保护;
对于可能被异步修改的变量,必须使用
volatile
声明;在临界区中尽量减少操作时间,避免影响系统实时性;
合理使用任务阻塞机制,避免忙等待浪费 CPU 资源。