第三章:任务划分与设计——如何合理划分任务,避免任务间耦合与死锁
好,咱们进入第三章。这一章聊的是任务划分,说白了就是怎么把一堆活儿拆开,分给不同的任务去干。你想想看,麻醉机里同时要跑的事情太多了——传感器采集、阀门控制、报警检测、人机交互……如果任务划分得不好,轻则系统卡顿,重则直接死锁。我在项目里见过太多次了,嗯,这里要重点讲。
3.1 任务划分的核心原则
我个人习惯把任务划分总结成四个字:高内聚,低耦合。什么意思?就是每个任务只干一件事,任务之间尽量少说话。
核心原则:
- 单一职责:一个任务只做一件事。比如“读取压力传感器”和“控制呼气阀”必须分开。
- 时间确定性:实时任务必须保证在规定时间内完成。麻醉机的呼吸周期是固定的,你不能让一个任务拖了整个周期。
- 资源独立性:尽量让每个任务独享自己的资源(比如内存、外设),避免共享带来的竞争。
我记得有一次,一个同事把“读取流量传感器”和“计算潮气量”放在同一个任务里。结果呢?传感器偶尔抖动,计算卡了一下,整个呼吸周期都乱了。这就是典型的耦合问题。
3.2 常见的任务划分方法
实际开发中,我一般用两种方法:事件驱动型和时间触发型。麻醉机这种生命支持设备,我建议混合使用。
| 类型 | 适用场景 | 例子 |
|---|---|---|
| 事件驱动型 | 响应外部中断或用户操作 | 按键按下、报警触发 |
| 时间触发型 | 周期性任务,固定时间间隔执行 | 每10ms采集一次压力、每5ms更新一次显示 |
举个例子,麻醉机的呼吸控制必须是时间触发型的——吸气时间、呼气时间都是固定的。而用户调节参数,比如旋钮转动,那就是事件驱动。这两者如果混在一起,很容易出问题。
我的建议:把时间关键的任务(比如呼吸控制)放在高优先级的时间触发任务里。把非关键任务(比如日志记录)放在低优先级的事件驱动任务里。这样即使日志任务卡了,呼吸也不会停。
3.3 如何避免任务间耦合
耦合是死锁的温床。我见过最典型的耦合就是共享全局变量。两个任务同时读写一个变量,不加保护,结果就是数据错乱,系统崩溃。
怎么解?我推荐用消息队列或者信号量。任务之间只通过消息传递数据,不直接访问对方的资源。
// 错误示范:直接共享全局变量
int pressure_value;
void task_sensor() {
pressure_value = read_sensor(); // 写
}
void task_control() {
if (pressure_value > 30) { // 读
close_valve();
}
}
// 正确示范:通过消息队列传递
osMessageQDef(pressure_queue, 10, int);
osMessageQId pressure_queue_id;
void task_sensor() {
int val = read_sensor();
osMessagePut(pressure_queue_id, val, 0); // 发送消息
}
void task_control() {
int val;
osMessageGet(pressure_queue_id, &val, osWaitForever); // 接收消息
if (val > 30) {
close_valve();
}
}
你看,用了消息队列之后,两个任务完全解耦了。传感器任务只管发,控制任务只管收。谁也不会干扰谁。
注意:消息队列虽然好用,但别滥用。队列长度要合理设置,太短会丢消息,太长浪费内存。我曾经在一个项目里把队列设成1,结果高频率发送时直接溢出,查了两天才找到原因。
3.4 死锁的预防与处理
死锁,说白了就是两个任务互相等对方释放资源,结果谁也别想动。在麻醉机里,死锁是绝对不能发生的——病人等着通气呢。
死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。只要打破其中一个,死锁就解了。
我常用的方法:
- 避免嵌套锁:如果一个任务需要多个锁,尽量一次性申请,或者按固定顺序申请。比如总是先拿锁A再拿锁B,不要反过来。
- 使用超时机制:申请锁时设置超时时间,超时了就放弃,而不是死等。
- 减少锁的粒度:能用信号量就别用互斥锁,能用原子操作就别用信号量。
// 死锁示例:两个任务互相等
// 任务1:拿锁A,等锁B
osMutexAcquire(mutex_A, osWaitForever);
osMutexAcquire(mutex_B, osWaitForever); // 如果任务2已经拿了锁B,这里就死锁了
// 任务2:拿锁B,等锁A
osMutexAcquire(mutex_B, osWaitForever);
osMutexAcquire(mutex_A, osWaitForever); // 死锁!
// 解决方法:统一锁的顺序
// 两个任务都先拿锁A,再拿锁B
osMutexAcquire(mutex_A, osWaitForever);
osMutexAcquire(mutex_B, osWaitForever);
我曾经在一个呼吸机项目里遇到过死锁。当时是报警任务和显示任务同时操作一个共享缓冲区,一个写一个读,结果互相等。最后我加了一个超时机制,超时后报警任务直接丢弃数据,显示任务继续显示旧数据。虽然数据可能丢一帧,但系统不会死。
避坑指南:我曾经在调试时发现系统偶尔卡死,查了三天才发现是一个中断服务程序里调用了信号量。记住,中断里绝对不要用阻塞型API!信号量、消息队列这些,在中断里只能用非阻塞版本(比如“尝试获取,失败就返回”)。
3.5 实战建议:麻醉机的任务划分示例
最后,我给出一个麻醉机实际的任务划分方案,供你参考:
| 任务名称 | 优先级 | 周期/触发方式 | 功能描述 |
|---|---|---|---|
| 呼吸控制任务 | 最高 | 时间触发,每1ms | 控制吸气阀、呼气阀,维持呼吸周期 |
| 传感器采集任务 | 高 | 时间触发,每5ms | 读取压力、流量、氧浓度传感器 |
| 报警检测任务 | 高 | 事件触发 | 检测异常值,触发声光报警 |
| 人机交互任务 | 中 | 事件触发 | 处理按键、旋钮、触摸屏输入 |
| 显示更新任务 | 低 | 时间触发,每50ms | 刷新屏幕上的波形和参数 |
| 日志记录任务 | 低 | 事件触发 | 记录操作日志和报警日志到Flash |
你看,呼吸控制任务优先级最高,周期最短。日志任务优先级最低,即使偶尔被抢断也不影响安全。任务之间通过消息队列传递数据,没有共享变量。这样设计,耦合度低,死锁风险也小。
嗯,这一章就到这里。记住一句话:任务划分不是拍脑袋,而是基于系统的时间要求和资源约束来设计的。下一章我们聊聊中断管理,那又是另一个坑了。