在多线程应用中,多个线程并发访问共享资源会引发竞态条件,导致数据异常与程序崩溃。nRF Connect SDK 基础课程第八课线程同步系统讲解线程同步的必要性、信号量(Semaphore)与互斥锁(Mutex)的核心特性,并通过两个经典实操实验,完整演示如何解决资源计数与临界区保护问题,是构建稳定可靠多线程系统的必备核心内容。
课程定位
本课是多线程开发的安全基石。在前序课程掌握线程创建、调度、优先级与工作队列后,本课聚焦并发安全,解决多线程访问共享资源的核心痛点。通过学习信号量与互斥锁,你将能够设计无冲突、无死锁、数据可靠的并发系统,是工业级嵌入式固件开发的必备技能。
核心学习目标
- 理解线程同步的必要性,认识临界区与竞态条件
- 掌握信号量的特性、API 与资源计数应用场景
- 掌握互斥锁的特性、API 与临界区保护应用场景
- 区分信号量与互斥锁的适用场景,正确选择同步机制
- 通过两个实操实验,独立实现资源管理与临界区保护
- 理解所有权、优先级继承等关键概念
一、线程同步的必要性
多线程环境下,临界区(Critical Section) 是多个线程可能同时访问的共享资源(变量、结构体、硬件接口、代码段)。若不加保护地并发访问,会出现:
- 数据读写被打断
- 计算结果错误
- 程序行为异常
- 系统崩溃
线程同步确保同一时间只有一个线程执行临界区,保证操作原子性与数据一致性。Zephyr RTOS 提供两种核心同步机制:
- 信号量(Semaphore):资源计数与同步信号
- 互斥锁(Mutex):独占式临界区保护
二、信号量(Semaphore)—— 资源计数与同步信号
信号量是带计数的同步机制,用于管理有限数量的共享资源。
核心特性
- 初始化时设定初始值与最大值
- k_sem_give():释放资源,计数 + 1(可在线程 / ISR 调用)
- k_sem_take():申请资源,计数 - 1,计数为 0 时阻塞(仅在线程调用)
- 无所有权:任意线程可 give,任意线程可 take
- 无优先级继承
- 适用于:资源计数、生产者消费者、同步信号
核心 API
#include <zephyr/kernel.h>
// 静态定义信号量:名称,初始计数,最大值
K_SEM_DEFINE(instance_monitor_sem, 10, 10);
// 申请资源:等待时间K_FOREVER永久阻塞
k_sem_take(&instance_monitor_sem, K_FOREVER);
// 释放资源
k_sem_give(&instance_monitor_sem);
// 获取当前计数
k_sem_count_get(&instance_monitor_sem);
三、互斥锁(Mutex)—— 独占式临界区保护
互斥锁是二值锁,用于保护必须独占访问的临界区,同一时间只允许一个线程持有。
核心特性
- 状态:locked/unlocked
- 严格所有权:只有加锁线程能解锁
- 可递归锁:同一线程可多次加锁,需对应次数解锁
- 仅在线程使用,不可在 ISR 使用
- 支持优先级继承,避免优先级反转
- 适用于:共享变量、共享代码段、硬件独占访问
核心 API
#include <zephyr/kernel.h>
// 静态定义互斥锁
K_MUTEX_DEFINE(test_mutex);
// 加锁:永久等待
k_mutex_lock(&test_mutex, K_FOREVER);
// 解锁
k_mutex_unlock(&test_mutex);
四、信号量 vs 互斥锁 对比表
表格
特性 | 信号量 | 互斥锁 |
功能 | 资源计数、同步信号 | 独占临界区保护 |
取值 | 0~ 最大值 | 锁定 / 解锁 |
所有权 | 无 | 有 |
调用者 | give 可 ISR,take 仅线程 | 仅线程 |
优先级继承 | 无 | 有 |
典型场景 | 资源池、生产者消费者 | 共享变量、设备访问 |
实操练习 1:信号量实现资源计数管理
目标:使用信号量限制资源最大数量,解决生产者消费者模型中的资源越界问题。
步骤 1:工程准备
打开基础工程 l8/l8_e1
步骤 2:定义线程优先级
#define PRODUCER_PRIORITY 5
#define CONSUMER_PRIORITY 5
步骤 3:定义共享资源(未加保护前会异常)
volatile uint32_t available_instance_count = 10;
步骤 4:生产者与消费者线程
void producer(void)
{
printk("Producer thread started\n");
while (1) {
release_access();
k_msleep(sys_rand32_get() % 10);
}
}
void consumer(void)
{
printk("Consumer thread started\n");
while (1) {
get_access();
k_msleep(sys_rand32_get() % 10);
}
}
步骤 5:资源申请与释放(无保护)
void get_access(void)
{
available_instance_count--;
printk("Resource taken: %d\n", available_instance_count);
}
void release_access(void)
{
available_instance_count++;
printk("Resource given: %d\n", available_instance_count);
}
现象:资源计数超过 10,数据异常。
步骤 6:加入信号量保护
// 定义信号量:初始10,最大10
K_SEM_DEFINE(instance_monitor_sem, 10, 10);
步骤 7:使用信号量保护资源操作
void get_access(void)
{
k_sem_take(&instance_monitor_sem, K_FOREVER);
printk("Resource taken: %d\n", k_sem_count_get(&instance_monitor_sem));
}
void release_access(void)
{
k_sem_give(&instance_monitor_sem);
printk("Resource given: %d\n", k_sem_count_get(&instance_monitor_sem));
}
步骤 8:注释掉原始变量
// volatile uint32_t available_instance_count = 10;
步骤 9:编译烧录
结果:资源计数严格保持在 0~10,生产者消费者稳定运行,无越界。
实操练习 2:互斥锁保护临界区,消除竞态
目标:使用互斥锁保护共享变量与代码段,确保 increment_count + decrement_count = COMBINED_TOTAL 恒成立。
步骤 1:工程准备
打开基础工程 l8/l8_e2
步骤 2:启用多线程
CONFIG_MULTITHREADING=y
步骤 3:定义线程优先级
#define THREAD0_PRIORITY 4
#define THREAD1_PRIORITY 4
步骤 4:定义共享变量
#define COMBINED_TOTAL 40
int32_t increment_count = 0;
int32_t decrement_count = COMBINED_TOTAL;
步骤 5:临界区代码(未保护会出现竞态)
void shared_code_section(void)
{
increment_count += 1;
increment_count = increment_count % COMBINED_TOTAL;
decrement_count -= 1;
if (decrement_count == 0) {
decrement_count = COMBINED_TOTAL;
}
}
步骤 6:竞态检测
uint8_t race_condition = 0;
int32_t inc_copy, dec_copy;
if (increment_count + decrement_count != COMBINED_TOTAL) {
race_condition = 1;
inc_copy = increment_count;
dec_copy = decrement_count;
}
if (race_condition) {
printk("Race condition!\n");
printk("%d + %d = %d\n", inc_copy, dec_copy, inc_copy+dec_copy);
k_msleep(400 + sys_rand32_get() % 10);
}
步骤 7:单线程运行正常,双线程立即出现竞态
现象:和不为 40,数据错误。
步骤 8:加入互斥锁保护
K_MUTEX_DEFINE(test_mutex);
步骤 9:临界区加锁 / 解锁
void shared_code_section(void)
{
// 加锁
k_mutex_lock(&test_mutex, K_FOREVER);
increment_count += 1;
increment_count = increment_count % COMBINED_TOTAL;
decrement_count -= 1;
if (decrement_count == 0) {
decrement_count = COMBINED_TOTAL;
}
// 检测并拷贝
if (increment_count + decrement_count != COMBINED_TOTAL) {
race_condition = 1;
inc_copy = increment_count;
dec_copy = decrement_count;
}
// 解锁
k_mutex_unlock(&test_mutex);
// 解锁后打印
if (race_condition) {
printk("Race condition!\n");
printk("%d + %d = %d\n", inc_copy, dec_copy, inc_copy+dec_copy);
k_msleep(400 + sys_rand32_get() % 10);
}
}
步骤 10:编译烧录
结果:无论多少线程并发,和永远等于 40,竞态完全消除。
课后测评
测评范围:
- 线程同步的必要性
- 信号量特性、API、适用场景
- 互斥锁特性、API、适用场景
- 所有权与优先级继承
- 竞态条件与临界区保护
- 信号量与互斥锁的选择
本课学习价值
本课是多线程系统稳定运行的最后一块拼图:
- 学会识别临界区与竞态条件
- 能用信号量实现资源计数、生产者消费者模型
- 能用互斥锁实现独占临界区保护
- 正确区分信号量与互斥锁,按需选择
- 构建无数据冲突、无异常崩溃的高可靠性嵌入式系统
掌握本课内容,你就具备了开发工业级多线程并发系统的核心能力。