从零搭建自动驾驶中间件(三):事件驱动与协程调度的工程实践

这是系列第三篇。前两篇聊了"为什么自研"和"共享内存通信",这篇聊调度------有了数据通道之后,如何让模块高效地跑起来。

1. 调度问题:从操作系统视角看自动驾驶

自动驾驶系统本质上是一个实时数据处理管道

复制代码
传感器 → 预处理 → 感知 → 跟踪 → 预测 → 规划 → 控制
 10Hz     10Hz    10Hz    10Hz   10Hz   10Hz   100Hz

调度要解决的核心问题是:在有限的 CPU 核心上,如何安排这些模块的执行顺序和时机,使得端到端延迟最小?

1.1 三种调度模型

模型 原理 优点 缺点
一模块一线程 每个模块独立线程 实现简单 线程数多、切换开销大、难以协调
线程池 + 任务队列 所有模块作为任务提交到线程池 线程数可控 模块间等待需要条件变量,上下文切换开销
协程调度 M 个协程运行在 N 个线程上 轻量切换、可挂起/恢复 需要协程库支持

HyperFlow 选择协程调度


2. 协程调度:为什么选 marl?

2.1 什么是 marl?

marl 是 Google 开源的 C++ 协程调度库,最初为 Vulkan/Dawn 项目设计。它的核心特性:

  • M:N 调度:M 个协程(Fiber)运行在 N 个 OS 线程上
  • 抢占式 :基于 marl::Scheduler 的协作式调度
  • 轻量:协程切换只需保存/恢复寄存器,约 100ns
  • 零依赖:纯 C++ 实现,不依赖 OS 特性

2.2 为什么不用其他的?

方案 问题
std::thread + 条件变量 每个模块一个线程,10 个模块就 10 个线程,调度不可控
boost::asio + strand 可行但学习曲线陡,且无法方便地挂起/恢复
libco(微信) 只支持 Linux x86_64,ARM 支持不好
goroutine(Go) 需要 GC,不适合嵌入式实时场景

marl 的优势在于:轻量、跨平台、API 简洁、协程切换快

2.3 marl 的核心 API

cpp 复制代码
// 初始化调度器
marl::Scheduler scheduler;
scheduler.setWorkerThreadCount(4);  // 4 个工作线程
scheduler.bind();

// 提交任务
scheduler.enqueue([] {
    LOG(INFO) << "Hello from fiber!";
});

// 等待/唤醒
marl::Event event;
scheduler.enqueue([&] {
    event.wait();  // 挂起当前协程,让出 CPU
    LOG(INFO) << "Woken up!";
});
event.signal();  // 唤醒等待的协程

scheduler.unbind();

3. HyperFlow 的调度架构

3.1 整体架构

复制代码
┌──────────────────────────────────────────────────────┐
│                   marl::Scheduler                      │
│                   (4 个工作线程)                        │
│                                                       │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │ Fiber A │  │ Fiber B │  │ Fiber C │  │ Fiber D │ │
│  │Module1  │  │Module2  │  │Module3  │  │Module4  │ │
│  │定时100ms│  │事件驱动  │  │定时 50ms│  │混合模式  │ │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘ │
│                                                       │
│  Worker1        Worker2        Worker3       Worker4   │
└──────────────────────────────────────────────────────┘

每个 Module 运行在一个 marl::Fiber 中,由 marl::Scheduler 调度。

3.2 模块的运行主循环

cpp 复制代码
ModuleImpl::run() {
    // Init 阶段已在主线程完成

    // Start
    module_->Start();

    while (running_) {
        if (interval_ > 0) {
            // 定时模式:等待 interval 毫秒
            marl::Event::wait_for(std::chrono::milliseconds(interval_));
        } else {
            // 事件驱动模式:等待 Trigger 唤醒
            event_.wait();  // marl::Event,挂起协程
        }

        // 执行 Step
        run_step();
    }

    // Stop
    module_->Stop();
}

3.3 三种调度模式

模式一:定时调度(interval > 0)

json 复制代码
{ "name": "lidar_driver", "interval": 100 }
复制代码
时间轴: 0ms    100ms   200ms   300ms   400ms
        │       │       │       │       │
Step:   [执行]  [执行]  [执行]  [执行]  [执行]

适用于传感器驱动等需要固定周期的模块。

模式二:事件驱动(interval = -1,listen trigger)

json 复制代码
{ "name": "perception", "listen": "lidar_driver.data_ready" }
复制代码
时间轴: 0ms              105ms             210ms
        │                 │                 │
        │  lidar_driver   │  lidar_driver   │
        │  Notify()       │  Notify()       │
        │       │         │       │         │
        │       ▼         │       ▼         │
Step:   [等待...] [执行]  [等待...] [执行]

适用于依赖上游数据的算法模块,避免空转浪费 CPU。

模式三:混合模式(Wait 带超时)

cpp 复制代码
void Step() override {
    if (Wait(100)) {
        // 被 Trigger 唤醒
        ProcessNewData();
    } else {
        // 超时,执行心跳逻辑
        CheckHealth();
    }
}

既响应事件,又有超时保护。适用于需要心跳检测的场景。


4. Trigger 机制:模块间的协作

4.1 NotifyTrigger 与 ListenTrigger

复制代码
Module A (Producer)                    Module B (Consumer)
    │                                       │
    │ Step() {                              │ Wait() { ... }
    │   Write("data", ...);                 │     │
    │   Notify("data_ready");  ──────►      │     ▼ 被唤醒
    │ }                                     │ Step() {
    │                                       │   Read("data", ...);
    │                                       │ }
    ▼                                       ▼

NotifyTrigger

  • 由写者模块拥有
  • Notify() 递增计数器,向所有监听进程发送 POSIX 信号
  • 支持 GetCount() 查看通知次数

ListenTrigger

  • 由读者模块绑定
  • Listen(module) 注册监听模块
  • WakeUp() 唤醒所有监听模块(调用 Module::Wakeup()

4.2 跨进程触发

复制代码
Process A                               Process B
  │                                       │
  │ Module1.Notify("data_ready")          │
  │     │                                 │
  │     ▼                                 │
  │ NotifyTrigger                         │
  │   │ Signal(pid_B, trigger_id)         │
  │   └──────────────────────────────────►│
  │                                       │ TriggerManager
  │                                       │   │ on_signal(trigger_id)
  │                                       │   ▼
  │                                       │ ListenTrigger.WakeUp()
  │                                       │   │
  │                                       │   ▼
  │                                       │ Module3.Wakeup()
  │                                       │   │ event_.signal()
  │                                       │   ▼
  │                                       │ Module3.Step() → Read("data")

关键细节

  1. NotifyTrigger 维护一个 pid_count,记录有多少进程在监听
  2. Signal::Send(pid, data) 向指定进程发送实时信号
  3. 接收端的 TriggerManager 在独立线程中 sigwait,收到信号后查找 Trigger 表并唤醒对应模块

4.3 Trigger 的命名约定

复制代码
module_name.trigger_name

例如:
  lidar_driver.data_ready
  fusion.result_ready
  control.cmd_sent

配置中的 listen 字段格式为 source_module.trigger_name

json 复制代码
{
  "name": "perception",
  "listen": "lidar_driver.data_ready"
}

5. 模块生命周期与插件架构

5.1 Module 生命周期

复制代码
Init(config) → Start() → [Step() × N] → Stop() → Destroy()
   主线程         任务线程      任务线程      任务线程     主线程
方法 线程 说明 是否必须实现
Init(config) 主线程 解析配置、初始化资源
Start() 任务线程 启动后回调 否(有默认实现)
Step() 任务线程 周期执行的业务逻辑
Stop() 任务线程 停止回调
Destroy() 主线程 资源释放

为什么 Init 和 Destroy 在主线程?

  • Init 时需要创建共享内存、绑定 DataQueue,这些操作需要全局协调
  • Destroy 时需要释放资源、关闭共享内存,必须等所有模块 Stop 之后

5.2 插件加载与模块注册

cpp 复制代码
// 在插件 .so 中定义模块
class MyPerception : public Module {
public:
    using Module::Module;

    std::error_code Init(const Json::Value &config) override {
        // 解析配置
        return std::error_code();
    }

    void Step() override {
        // 读取点云 → 运行推理 → 写出结果
        const PointCloud* cloud = nullptr;
        Get("lidar_points", cloud);
        // ...
        Write("detection_result", result);
        Notify("detection_ready");
    }
};

// 注册模块
REGISTER_MODULE(MyPerception, "MyPerception");

注册机制

  1. REGISTER_MODULE 宏在 .so 加载时(dlopen)的静态初始化阶段执行
  2. "MyPerception"MyPerception::Create 的映射注册到全局工厂
  3. ModuleManager 根据配置中的 type 字段从工厂创建模块实例
cpp 复制代码
// REGISTER_MODULE 展开后等价于:
struct MyPerceptionRegister {
    MyPerceptionRegister() {
        ModuleRegister::Register<MyPerception>("MyPerception");
    }
};
static MyPerceptionRegister g_register;

5.3 Pimpl 模式

Module 使用 Pimpl(Pointer to Implementation)模式隐藏内部实现:

cpp 复制代码
class Module {
public:
    // 公共接口
    std::error_code Write(const std::string &data_name, const void *data, uint32_t size);
    std::error_code Read(const std::string &data_name, void *buf, ...);
    void Notify(const std::string &trigger_name);
    bool Wait(uint32_t ms);

private:
    std::unique_ptr<ModuleImpl> impl_;  // 隐藏实现细节
};

ModuleImpl 持有运行时状态:

cpp 复制代码
class ModuleImpl {
    std::atomic<bool> running_;
    std::string name_;
    int interval_;
    uint32_t max_exec_time_;
    uint16_t module_id_;
    bool enable_snapshot_;
    bool fillback_;
    marl::Event event_;                          // 协程等待/唤醒
    std::unordered_map<std::string, std::shared_ptr<NotifyTrigger>> notify_triggers_;
    std::unordered_map<std::string, std::shared_ptr<DataQueue>> data_queues_;
    DataSnapshot snapshot_;
};

Pimpl 的好处

  • Module 的头文件不需要暴露 marl、DataQueue 等实现细节
  • 修改 ModuleImpl 不需要重新编译依赖 Module 的代码
  • 编译时间大幅减少

6. 执行超时监控

自动驾驶系统中,模块执行超时可能导致安全风险。HyperFlow 内建了超时监控:

cpp 复制代码
ModuleImpl::run_step() {
    auto start = utils::GetTimestampUs();

    module_->Step();  // 执行用户逻辑

    auto elapsed = utils::GetTimestampUs() - start;
    stat_.exec_time.Update(elapsed);

    if (elapsed > max_exec_time_ * 1000) {
        LOG(WARNING) << "Module " << name_
                     << " execution timeout: " << elapsed << "us"
                     << " > " << max_exec_time_ * 1000 << "us";
    }
}

通过 Telnet 可以查看模块的执行统计:

复制代码
$ telnet localhost 8800
> cd modules/perception
> exec_stat
name:            perception
tick_count:      12345
avg_exec_time:   234 us
max_exec_time:   567 us
min_exec_time:   120 us

7. 与 CyberRT 调度的对比

维度 HyperFlow CyberRT
协程库 marl Croutine
调度策略 定时 / 事件驱动 / 混合 Classic / Choreography
优先级 不支持 Choreography 支持
CPU 亲和 不支持 Choreography 支持
DAG 感知 不支持 支持(通过 DAG 配置)
模块间协作 Trigger (Notify/Listen) Channel + Event
跨进程通知 POSIX 实时信号 SHM Transport
线程模型 marl M:N Croutine M:N + 调度线程

CyberRT 的 Choreography 策略确实更强------它可以根据优先级和 CPU 亲和性安排协程到特定核心,对延迟敏感的模块(如控制)给更高优先级。

HyperFlow 的取舍:优先级和 CPU 亲和是"好东西",但实现复杂度高,且大多数场景下通过合理的 interval 配置 + 事件驱动就能满足延迟要求。如果后续确实需要,可以在此基础上扩展。


8. 踩坑总结

  1. marl 的 bind/unbind :每个线程使用 marl 前必须 scheduler.bind(),否则 marl::Event::wait() 会死锁
  2. 协程中不能调用阻塞操作sleep()mutex::lock() 等会阻塞 OS 线程,导致同一线程上的其他协程也无法运行。应该使用 marl::Event::wait_for() 代替 sleep()
  3. 信号处理线程安全sigwait 必须在 sigaction 之前设置,否则信号可能被默认处理器捕获
  4. Trigger 的命名冲突 :不同模块的 notify 名称不能重复,否则 TriggerManager 会混淆
  5. Step 中的长耗时操作 :如果 Step() 执行时间超过 interval,模块会自动跳过下一个调度周期,不会堆积

9. 小结

调度层是自动驾驶中间件的"心脏"。HyperFlow 的设计选择了轻量 + 灵活的路线:

  • marl 协程:M:N 调度,轻量切换
  • 双模式调度:定时 + 事件驱动,覆盖绝大多数场景
  • Trigger 机制:简洁的模块间协作,支持跨进程
  • 插件架构:模块与框架解耦,可独立编译

这不是唯一的方案,CyberRT 的优先级调度在复杂场景下更优。但对于大多数自动驾驶项目来说,HyperFlow 的方案已经足够,且实现和维护成本低得多。

下一篇,我们将聊自动驾驶算法调试的核心基础设施------数据录制与回灌。


下期预告:《从零搭建自动驾驶中间件(四):数据录制与回灌------算法调试的核心基础设施》

相关推荐
叶子Talk1 小时前
GPT-5.5幻觉率骤降52.5%,但90%的公司还在裸奔?
人工智能·gpt·ai·openai·gpt-5.5·幻觉率
fangzt20101 小时前
从零搭建自动驾驶中间件(五):状态机、诊断与运维——让系统“可观测、可控制“
中间件·自动驾驶
人工智能AI技术1 小时前
栈与队列基础:应用场景与经典面试题
人工智能
fangzt20101 小时前
从零搭建自动驾驶中间件(四):数据录制与回灌——算法调试的核心基础设施
算法·中间件·自动驾驶
荔枝学Python1 小时前
Agent设计最强书籍:它真的把Agent讲解的非常透彻!!
人工智能·程序员·大模型·大语言模型·agent·ai大模型·智能体
YJlio1 小时前
OpenClaw v2026.4.23 更新了哪些内容?图像生成、鉴权路由、媒体持久化与排障修复深度解析
人工智能·开源项目·自动化运维·版本更新·ai agent·openclaw·gpt-image-2
YJlio1 小时前
OpenClaw v2026.4.24 更新了哪些内容?Google Meet、DeepSeek V4、实时语音与浏览器自动化深度解析
人工智能·开源项目·版本更新·ai agent·deepseek·openclaw·v4 自动化运维
QD_ANJING1 小时前
建议5月的Web前端开发都去飞书上准备面试...
前端·人工智能·面试·职场和发展·前端框架·状态模式·ai编程