第三章:任务划分与设计——如何合理划分任务,避免任务间耦合与死锁

好,咱们进入第三章。这一章聊的是任务划分,说白了就是怎么把一堆活儿拆开,分给不同的任务去干。你想想看,麻醉机里同时要跑的事情太多了——传感器采集、阀门控制、报警检测、人机交互……如果任务划分得不好,轻则系统卡顿,重则直接死锁。我在项目里见过太多次了,嗯,这里要重点讲。

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

你看,呼吸控制任务优先级最高,周期最短。日志任务优先级最低,即使偶尔被抢断也不影响安全。任务之间通过消息队列传递数据,没有共享变量。这样设计,耦合度低,死锁风险也小。

嗯,这一章就到这里。记住一句话:任务划分不是拍脑袋,而是基于系统的时间要求和资源约束来设计的。下一章我们聊聊中断管理,那又是另一个坑了。