嵌入式筑基之设计模式

嵌入式开发中,多任务通信用什么设计模式比较合适?

核心思想:优先使用操作系统提供的通信机制

在RTOS(实时操作系统)环境下,如FreeRTOS、Zephyr、RT-Thread等,其内置的通信原语本身就是某些设计模式的实现。我们的工作 often 是正确地选择和组合它们。


1. 生产者-消费者模式 (Producer-Consumer Pattern)

这是最常用、最基础的多任务通信模式。

  • 模式描述 :一个或多个任务(生产者)产生数据,另一个或多个任务(消费者)处理数据。它们不直接通信,而是通过一个共享的缓冲区(Buffer) 进行数据交换。
  • RTOS实现
    • 队列 (Queue) / 消息队列 (Message Queue):这是该模式最经典的实现。队列本身就是一个线程安全的缓冲区,处理了所有的同步和互斥问题。
    • 环形缓冲区 (Ring Buffer/Circular Buffer) :如果你在裸机或追求极致性能的场景下,可以自己实现一个环缓冲,但必须配合信号量 (Semaphore)互斥锁 (Mutex) 来保证线程安全。
  • 优点
    • 解耦:生产者和消费者彼此不知道对方的存在,只关心缓冲区。
    • 平衡速度差:生产者突然爆发性产生数据时,缓冲区可以缓存,消费者可以按自己的速度处理,避免任务长时间阻塞。
    • 易于扩展:可以方便地增加生产者或消费者的数量。
  • 适用场景
    • 数据采集系统(ADC采样->数据处理)
    • 事件处理系统(GUI事件->事件处理)
    • 命令解析(串口接收->命令解析任务)
    • 几乎所有异步数据传递的场景

2. 观察者模式 (Observer Pattern) / 发布-订阅模式 (Publish-Subscribe Pattern)

  • 模式描述:一个任务(发布者)发布消息或事件,多个任务(订阅者)接收并处理该消息。发布者不需要知道谁订阅了消息。
  • RTOS实现
    • 事件标志组 (Event Flags Group):每个比特可以代表一个特定的事件。多个任务可以等待同一组事件。非常适合状态机、事件触发等场景。
    • 消息队列 + 多个消费者:一个队列可以被多个任务读取(但通常一条消息只能被一个消费者取走)。要实现真正的广播(一条消息所有消费者都收到),需要更复杂的结构。
    • 软件消息总线 (Software Message Bus):在RTOS之上自己实现一个简单的消息中心,任务可以向总线发布消息或订阅特定主题的消息。
  • 优点
    • 松耦合:发布者和订阅者完全解耦。
    • 一对多通信:非常适合需要广播消息的场景。
  • 适用场景
    • 系统状态变化通知(如"网络连接已建立"、"电池电量低")。
    • 传感器数据更新(如"温度值已更新",多个任务可能关心此事件)。
    • 按键/输入事件广播。

3. 黑板模式 (Blackboard Pattern)

  • 模式描述:多个任务(知识源)可以自由地向一个共享的、结构化的全局内存区(黑板)读取或写入数据。通常由一个仲裁者来管理对黑板的访问。
  • RTOS实现
    • 共享内存 + 互斥锁 (Mutex):黑板就是一块共享内存。任务在访问前必须获取互斥锁,以确保数据的一致性。
    • 信号量 (Semaphore) 也可用于保护资源。
  • 优点
    • 灵活的数据共享:任何任务都可以访问任何数据。
  • 缺点
    • 容易导致耦合:任务之间通过共享的数据结构产生隐式耦合。
    • 风险高: improper use 容易导致竞态条件、死锁和数据损坏。
  • 适用场景
    • 需要极高效率的场合,且数据交换非常频繁。
    • 对性能要求极致,并且程序员能完全掌控任务调度和访问时序(常见于裸机程序或对实时性要求极高的部分)。
    • 慎用:在复杂的RTOS应用中,应优先使用队列等通信方式,而非共享内存。

4. 中介者模式 (Mediator Pattern)

  • 模式描述:所有任务之间的通信不直接进行,而是通过一个"中介者"对象来中转。这个中介者负责协调各个任务之间的交互。
  • RTOS实现 :可以创建一个专用的中介者任务 。其他任务都通过消息队列向中介者任务发送请求,中介者任务根据消息内容调用其他任务或服务的接口。
  • 优点
    • 将网状通信变为星形通信,大幅降低系统复杂度。
    • 集中控制:通信逻辑集中在中介者中,易于维护和扩展。
  • 缺点
    • 中介者任务可能成为性能瓶颈和单点故障。
    • 增加了通信延迟。
  • 适用场景
    • 系统模块众多,通信关系复杂,难以管理。
    • 需要集中管理某些全局操作或状态机。

总结与选择建议

模式 RTOS原语 优点 缺点 适用场景
生产者-消费者 队列 (Queue) 解耦、缓冲、安全 数据拷贝可能带来开销 最通用,异步数据流
发布-订阅 事件组 (Event Group) 一对多、松耦合 传递的数据量小(通常是事件标志) 状态、事件广播
黑板 互斥锁 (Mutex) + 共享内存 极高效、灵活 风险高、易耦合、难调试 极高频率数据共享(慎用)
中介者 队列 + 专用任务 简化复杂通信 可能成为瓶颈 复杂系统模块协调

如何选择?问自己这几个问题:

  1. 通信是单向还是双向?

    • 单向:生产者-消费者(队列)。
    • 双向/请求响应:通常用两个队列,一个用于请求,一个用于回复。
  2. 是一个对一,一对多,还是多对多?

    • 一对一:队列。
    • 一对多:事件组或发布-订阅模式。
    • 多对多:可能需要中介者或更复杂的消息总线。
  3. 数据量大小和频率如何?

    • 数据量大/频率高 :考虑共享内存(黑板模式),但必须做好保护。或者传递指针(指向数据的指针,而非数据本身)通过队列传递,但要万分小心内存生命周期。
    • 数据量小/频率低:直接使用队列传递数据本身,简单安全。
  4. 实时性要求如何?

    • 要求极高 :优先考虑中断服务程序(ISR)与任务间的通信,使用队列(支持从中断中发送)信号量。避免在ISR中使用互斥锁。

黄金法则:

  • 优先使用队列:在大多数情况下,消息队列是最好、最安全的选择。它内置同步,完美实现了生产者-消费者模式。
  • 状态/事件通知用事件组:当你只需要通知一个事件发生,而不需要传递大量数据时,事件标志组是最轻量、最高效的选择。
  • 保护共享资源用互斥锁 :当你不得不使用全局变量或共享内存时(黑板模式),必须用互斥锁来保护它。
  • 避免全局变量:尽量不要使用无保护的全局变量进行任务间通信,这是嵌入式系统不稳定的一大根源。

对于初学者和大多数应用,熟练掌握"队列"和"事件标志组" 这两样工具,就已经能优雅地解决95%以上的嵌入式多任务通信问题了。

相关推荐
Mercury_Lc1 小时前
【贪心 或 DFS - 面试题】小于n最大数
数据结构·c++·算法
凤年徐1 小时前
【数据结构】LeetCode160.相交链表 138.随即链表复制 牛客——链表回文问题
c语言·数据结构·c++·算法·leetcode·链表
拾忆,想起1 小时前
Redis发布订阅:实时消息系统的极简解决方案
java·开发语言·数据库·redis·后端·缓存·性能优化
AllyLi02241 小时前
CondaError: Run ‘conda init‘ before ‘conda activate‘
linux·开发语言·笔记·python
羑悻的小杀马特1 小时前
【C++高并发内存池篇】ThreadCache 极速引擎:C++ 高并发内存池的纳秒级无锁革命!
开发语言·c++·多线程·高性能内存池
布朗克1682 小时前
OpenTelemetry 在 Spring Boot 项目中的3种集成方式
java·开发语言·opentelemetry
jingfeng5142 小时前
线程池及线程池单例模式
linux·开发语言·单例模式
UrSpecial2 小时前
设计模式:责任链模式
设计模式·责任链模式
简色2 小时前
领悟8种常见的设计模式
java·后端·设计模式