前言
在上篇文章的信号量示例中,我们把信号量初始值设为 1,用做"二值信号量",也能实现互斥访问:
线程A take(信号量) → 使用资源 → release(信号量) 线程B take(信号量) → 阻塞等待 → …… → 被唤醒
但这有一个隐患:任何线程都可以 release 这个信号量,包括不是它持有者的第三方线程。这就好比你把厕所门锁了,外面的人却能把手伸进来开锁——那锁还有什么意义?
互斥量(Mutex)正是为了解决这个问题而生的。它的核心规则是:
谁上锁,就只能由谁开锁。
目录
前言
一、为什么要互斥?
1.1 临界资源
1.2 什么情况下需要互斥?
二、互斥量的特性
2.1 互斥量 vs 二值信号量
2.2 互斥量的核心优势
三、优先级反转与优先级继承
3.1 什么是优先级反转?
3.2 优先级继承 — RT-Thread 的解决方案
四、互斥量 API
4.1 创建/初始化
4.2 删除/脱离
4.3 获取/释放
五、完整示例
示例1:互斥量基本使用
示例2:优先级反转与继承演示
六、互斥量使用注意事项
七、IPC 全家桶总结(1~7篇)
八、总结
一、为什么要互斥?
1.1 临界资源
在多线程系统中,如果多个线程同时访问同一个资源,就可能出问题:
/* 两个线程同时调用这个函数,结果会怎样? */ static int a = 1; void add_a(void) { a = a + 8; // 内部三步:①读原值 ②加8 ③写回 }如果线程 A 读出了a=1(还没写回),这时线程 B 抢占了 CPU,也读了a=1,然后加 8 写回 →a=9。接着 A 继续执行,它手里的值还是 1,加 8 写回 →a=9。
期望结果是 17,实际结果是 9。这就是典型的非原子操作导致的竞态问题。
1.2 什么情况下需要互斥?
| 场景 | 例子 | 说明 |
|---|---|---|
| 访问外设 | 串口打印 | 两个线程同时打印,信息混杂在一起 |
| 修改变量 | a = a + 8 | 读-改-写三步,可能被中断 |
| 访问结构体 | 设置多个成员 | 设置到一半被抢占,结构体状态不一致 |
| 函数重入 | 使用全局变量的函数 | 多个线程同时调用,全局变量互相覆盖 |
这些需要被独占访问的资源,称为临界资源。访问临界资源的那段代码,称为临界区。
二、互斥量的特性
2.1 互斥量 vs 二值信号量
| 对比项 | 二值信号量 | 互斥量(Mutex) |
|---|---|---|
| 初始值 | 1/0 | 1 |
| 谁可以释放? | 任何线程/ISR都可以 | 只有持有者可以释放 |
| 中断中可用? | ✅ 可以 | ❌不可用 |
| 优先级继承 | ❌ 不支持 | ✅支持(解决优先级反转) |
| 本质 | 计数器(取 0 或 1) | 特殊的二值信号量+拥有者标记 |
2.2 互斥量的核心优势
- 信号量:线程A take → 串口打印 → 线程B 也能 release → 线程A 被"强制解锁"
- 互斥量:线程A take → 串口打印 → 只有线程A 能 release → 别人无法解锁
互斥量就像一把带指纹识别的锁——只有上锁的人才能打开。
三、优先级反转与优先级继承
3.1 什么是优先级反转?
假设有三个线程:
LPThread:低优先级(15)
MPThread:中优先级(14)
HPThread:高优先级(13)
正常预期:HPThread(13) > MPThread(14) > LPThread(15) 实际可能: ① LPThread 先获得互斥量 ② HPThread 也想获得互斥量 → 被阻塞(LPThread 不释放) ③ MPThread 不需要互斥量,但优先级比 LPThread 高 → MPThread 一直运行,LPThread 无法执行 → LPThread 无法释放互斥量 → HPThread 被 LPThread 堵住,又被 MPThread 间接堵住结果:最高优先级的 HPThread,被中优先级的 MPThread 间接阻塞——这叫优先级反转。
3.2 优先级继承 — RT-Thread 的解决方案
RT-Thread 的互斥量内置了优先级继承机制:
① LPThread 获得互斥量 → 优先级 = 15 ② HPThread 也想获得互斥量 → 失败 → HPThread 把 LPThread 的"优先级提升"到自己一样 → LPThread 的优先级从 15 提升到 13 ③ LPThread 变成最高优先级 → 立刻执行 ④ LPThread 释放互斥量 → 恢复自己的优先级 15 ⑤ HPThread 被唤醒 → 获得互斥量 → 执行
优先级继承 = 低优先级线程临时"借用"高优先级线程的优先级,以便尽快释放互斥量。
四、互斥量 API
4.1 创建/初始化
/* 动态创建 */ rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag); /* 静态初始化 */ rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag);⚠️注意:
flag参数已废除,均按RT_IPC_FLAG_PRIO处理(优先级继承必须基于优先级排序)。
4.2 删除/脱离
rt_err_t rt_mutex_delete(rt_mutex_t mutex); // 动态删除 rt_err_t rt_mutex_detach(rt_mutex_t mutex); // 静态脱离4.3 获取/释放
/* 获取互斥量(可阻塞) */ rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time); /* 尝试获取(不阻塞) */ rt_err_t rt_mutex_trytake(rt_mutex_t mutex); /* 释放互斥量(只能由持有者调用) */ rt_err_t rt_mutex_release(rt_mutex_t mutex);五、完整示例
示例1:互斥量基本使用
本程序创建 2 个发送线程,通过宏USE_MUTEX切换是否使用互斥量,观察串口打印是否混杂。
#include <rtthread.h> #define USE_MUTEX 1 // 1=使用互斥量,0=不使用 static rt_mutex_t dynamic_mutex = RT_NULL; static rt_thread_t thread1 = RT_NULL; static rt_thread_t thread2 = RT_NULL; #define THREAD_PRIORITY 15 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 10 /* 发送线程入口函数(两个线程共用) */ static void thread_entry(void *parameter) { rt_uint32_t i; while(1) { #ifdef USE_MUTEX /* 使用互斥量:完整打印 0~19,不会被其他线程打断 */ rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); for(i = 0; i < 20; i++) rt_kprintf("%s: i=%d\n", (char *)parameter, i); rt_mutex_release(dynamic_mutex); #else /* 不使用互斥量:打印到一半可能被其他线程打断 */ for(i = 0; i < 20; i++) rt_kprintf("%s: i=%d\n", (char *)parameter, i); #endif rt_thread_mdelay(10); } } int main(void) { /* 1. 创建动态互斥量 */ dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_FIFO); if (dynamic_mutex == RT_NULL) { rt_kprintf("rt_mutex_create error.\n"); return -1; } /* 2. 创建发送线程1 */ thread1 = rt_thread_create("thread1", thread_entry, "thread1", THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread1 != RT_NULL) rt_thread_startup(thread1); /* 3. 创建发送线程2 */ thread2 = rt_thread_create("thread2", thread_entry, "thread2", THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE); if (thread2 != RT_NULL) rt_thread_startup(thread2); return 0; }运行结果对比:
未用互斥量 用互斥量
不使用:实验现象如下图左边所示,线程 1、线程 2 的打印信息没有混在 一起,线程1打印0~7,还未到19,就被线程2打断;
使用:实验现象如下图右边所示,线程1和线程2,依次打印0~19,中途 不会相互打断;
示例2:优先级反转与继承演示
创建三个线程演示优先级继承机制:
#include <rtthread.h> static rt_mutex_t dynamic_mutex = RT_NULL; static rt_thread_t LPThread = RT_NULL; // 低优先级 15 static rt_thread_t MPThread = RT_NULL; // 中优先级 14 static rt_thread_t HPThread = RT_NULL; // 高优先级 13 #define THREAD_PRIORITY 15 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 10 /* 低优先级线程:获得互斥量,然后被 HPThread 提升优先级 */ static void lpthread_entry(void *parameter) { rt_kprintf("LPThread: get mutex\n"); rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); /* 此时的优先级可能已被 HPThread 提升 */ rt_kprintf("LPThread: do something\n"); rt_thread_mdelay(5); rt_mutex_release(dynamic_mutex); rt_kprintf("LPThread: release mutex\n"); } /* 中优先级线程:不需要互斥量 */ static void mpthread_entry(void *parameter) { rt_kprintf("MPThread: run\n"); rt_thread_mdelay(2); // 短暂运行后挂起 } /* 高优先级线程:需要互斥量 */ static void hpthread_entry(void *parameter) { rt_kprintf("HPThread: try to get mutex\n"); rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); rt_kprintf("HPThread: got mutex\n"); rt_mutex_release(dynamic_mutex); } int main(void) { /* 1. 创建动态互斥量 */ dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_FIFO); if (dynamic_mutex == RT_NULL) return -1; /* 2. 创建三个优先级不同的线程 */ LPThread = rt_thread_create("LPThread", lpthread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, // 15 THREAD_TIMESLICE); if (LPThread != RT_NULL) rt_thread_startup(LPThread); MPThread = rt_thread_create("MPThread", mpthread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY - 1, // 14 THREAD_TIMESLICE); if (MPThread != RT_NULL) rt_thread_startup(MPThread); HPThread = rt_thread_create("HPThread", hpthread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY - 2, // 13 THREAD_TIMESLICE); if (HPThread != RT_NULL) rt_thread_startup(HPThread); return 0; }执行流程:
| 时序 | 事件 | 优先级变化 |
|---|---|---|
| A | HPThread(13) 先运行,设置标志后休眠 3ms | 13 |
| B | MPThread(14) 运行,休眠 1ms | 14 |
| C | LPThread(15) 获得互斥量,开始运行 | 15 |
| D | MPThread 休眠结束 → 抢占 LPThread | 14 vs 15 |
| E | HPThread 休眠结束 → 需要互斥量 → 失败 →提升LPThread优先级 | LPThread: 15→13 |
| F | LPThread(13) 变成最高优先级 → 继续运行 → 释放互斥量 →恢复原优先级,唤醒 HPThread | LPThread: 13→15 |
| G | HPThread(13) 获得互斥量,执行完毕 | 13 |
六、互斥量使用注意事项
| 注意事项 | 说明 |
|---|---|
| ❌不能在中断中使用 | ISR 中不可调用rt_mutex_take/release |
| ✅只能由持有者释放 | 其他线程无法强制释放 |
| ✅支持优先级继承 | 自动解决优先级反转问题 |
| ✅嵌套取锁 | 同一个线程可以多次take,但需要相同次数的release |
| ⚠️尽快释放 | 持有互斥量期间不要做耗时操作 |
七、IPC 全家桶总结(1~7篇)
到现在为止,我们已经学完了 RT-Thread 的所有核心 IPC 机制:
| 文章 | 内容 | 传递什么 | 特点 |
|---|---|---|---|
| (一) | 线程创建与删除 | — | 线程基础 |
| (二) | 优先级与时间片 | — | 调度规则 |
| (三) | 延时函数与空闲线程 | — | 让出 CPU |
| (四) | 消息队列 | 任意大小数据 | 灵活,链表实现 |
| (五) | 邮箱 | 4 字节(整数/指针) | 轻量,数组实现 |
| (六) | 信号 | 信号编号 | 软中断,异步通知 |
| (六) | 信号量 | 1 个计数值 | 计数/同步 |
| (七) | 互斥量 | — | 资源互斥,优先级继承 |
八、总结
| 知识点 | 要点 |
|---|---|
| 互斥量 | 特殊的二值信号量,自带所有权标记 |
| 核心规则 | 谁 take,谁 release,别人无法释放 |
| 动态创建 | rt_mutex_create(name, flag)— flag 已废除 |
| 静态初始化 | rt_mutex_init(mutex, name, flag) |
| 获取 | rt_mutex_take()(可阻塞)/rt_mutex_trytake()(不阻塞) |
| 释放 | rt_mutex_release()— 只能是持有者调用 |
| 删除/脱离 | rt_mutex_delete()/rt_mutex_detach() |
| ⚠️ 中断中 | 不可使用互斥量 |
| ✅ 优先级继承 | 自动解决优先级反转问题 |
| 互斥量 vs 信号量 | 信号量用于计数/同步,互斥量用于资源互斥 |