重构 Dora 事件循环框架:告别 while,拥抱多线程

1 Dora 传统while/for 事件循环处理框架问题

传统 Dora 节点采用原生的 while/for 循环来同步串行处理事件。**在面对高频传感器输入、多话题输入或长耗时算法场景时,依靠while循环实现响应事件的机制会暴露出实时性差、扩展困难、线程安全隐患大以及生命周期管理混乱等核心技术痛点。**具体核心问题如下:

  1. 事件 理同步阻塞,延 极高 :所有事件处理器同步串行执行,一旦某个核心算法(如图像处理)耗时较长,整个主循环将直接卡死。在此期间新到达的高频事件(如Tick、IMU数据)无法被及时取出,全部堆积在底层队列中,导致系统整体接收延迟从微秒级飙升至长任务的执行时间;

  2. while/for循环处理事件的模式输入路由低效且冗余,依赖if/else判断事件ID,当输入事件增多后代码臃肿,查找效率低,且需重复编写事件解析代码;

3 无原生定时器能力,实现定时任务需手动编写守护线程、轮询逻辑,线程安全和生命周期管理完全依赖开发者手写;

5.无标准化的多监听者注册、管理机制,一个事件需触发多个处理逻辑时,要手动维护处理器列表并遍历调用;

  1. 生命周期与资源无自动管理功能,无法统一的资源回收逻辑,节点异常退出时易产生僵尸线程、资源泄漏,需手动处理线程关闭、资源释放等繁琐操作。

DORA框架中传统for/while 事件处理代码

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
#include <thread>
#include <cstring>

// Include your template's specific API header
#include "node_api.h" 

// Simulate a time-consuming algorithm processing function
void process_image(const char* data, size_t data_len) {
    std::cout << "[RawNode] Start processing image, data size: " << data_len << " bytes..." << std::endl;
    // Simulate a 200ms processing delay
    std::this_thread::sleep_for(std::chrono::milliseconds(200)); 
    std::cout << "[RawNode] Image processing completed." << std::endl;
}

int main() {
    // 1. Initialize the Dora context using your environment-based API
    void *dora_context = init_dora_context_from_env();
    if (dora_context == nullptr) {
        std::cerr << "[RawNode] init dora context failed" << std::endl;
        return -1;
    }

    std::cout << "[RawNode] Dora context initialized. Entering event loop..." << std::endl;

    // 2. Traditional blocking infinite while loop
    while (1) {
        // Fetch the next event via the context pointer
        void *event = dora_next_event(dora_context);
        if (event == nullptr) {
            std::cerr << "[RawNode] ERROR: unexpected end of event" << std::endl;
            return -1;
        }

        // Read the event type using the template's enum scheme
        enum DoraEventType ty = read_dora_event_type(event);

        // 3. Branching logic using if/else if condition checks
        if (ty == DoraEventType_Input) {
            char *id;
            size_t id_len;
            read_dora_input_id(event, &id, &id_len);
            
            char *data;
            size_t data_len;
            read_dora_input_data(event, &data, &data_len);

            // Construct a temporary C++ string for cleaner topic matching
            std::string input_id(id, id_len);

            // Routing data streams based on the text ID
            if (input_id == "image_task") {
                // [PAIN POINT]: This synchronous call blocks the entire thread for 200ms.
                // Any 'tick' events arriving during this period will buffer up in the queue.
                process_image(data, data_len);
            } 
            else if (input_id == "tick") {
                std::cout << "[RawNode] Received periodic tick signal." << std::endl;
            }
        } 
        else if (ty == DoraEventType_Stop) {
            std::cout << "[RawNode] Received stop event (STOP)." << std::endl;
            free_dora_event(event); // Prevent memory leaks before escaping
            break; 
        } 
        else {
            std::cerr << "[RawNode] Received unexpected event type: " << ty << std::endl;
            free_dora_event(event);
            break;
        }

        // Always release the event structure memory at the end of each iteration
        free_dora_event(event);
    }

    std::cout << "[RawNode] Exited event loop. Cleaning up context..." << std::endl;
    
    // 4. Clean resource teardown
    free_dora_context(dora_context);
    
    std::cout << "[RawNode] Finished successfully." << std::endl;
    return 0;
}

2 DORA SerialEventLoop框架

SerialEventLoop 框架每个****input_id 对应一个专属线程,同源事件在该线程内严格串行执行 。通过两条核心设计约束,实现架构功能:1.一个 input_id 仅绑定一个 handler------ 无需实现事件多播、handler 去重等复杂逻辑;2.一个 handler 独占一个专属线程 ------ 无需线程池调度,从根源上避免了同源事件的并发问题。两条约束让 SerialEventLoop 避免CCDoraEventLoop 中线程池、Future 追踪、通用 Event 模板等复杂抽象,仅用极少代码实现事件响应核心需求。

2.1 整体架构

SerialEventLoop框架分为两层:SerialWorker )和 SerialEventLoop 。其中,SerialWorker 执行层采用了专属线程+独立事件队列的策略;SerialEventLoop采用事件接收 + 分发 + 定时器 + 输出队列的策略。

SerialWorker 执行层核心执行链路是主循环 ──enqueue()──▶ [事件队列] ──condition_variable──▶ [专属线程] ──handler(event)。每个 SerialWorker 实例持有 1 个 std::thread 专属线程、1 个 std::queue<InputEvent> 事件队列和 1 个 std::condition_variable 条件变量。主循环调用 enqueue() 写入事件并发送通知,专属线程被唤醒后从队列取出事件、执行绑定的 handler。实现事件严格按到达顺序依次处理和异源事件完全并行执行的功能

SerialEventLoop 运行在主线程中,负责事件接收与分 定****时 线 程管理定****时 线 程管理

  1. 事件接收与分 :阻塞等待 dora_next_event 获取事件,根据 input_id 哈希查表找到对应 SerialWorker,调用 enqueue() 完成事件分发;整个分发过程仅为 O (1) 复杂度的哈希查找,瞬间完成后立即回到等待下一个事件的状态,不会成为性能瓶颈。
  2. 线 程管理 :启动 1 个独立的 std::thread 线程,按 10ms 粒度轮询已注册的定时器,到期后自动触发绑定的回调函数;支持重复定时器和一次性定时器(触发后自动移除),定时器线程与主事件循环完全解耦,互不影响。
  3. 线 程管理 :外部 handler 调用 send_output() 时,框架会自动检测当前线程身份:若在主线程且处于事件循环内,直接调用 dora_send_output 发送;否则将输出内容入队,由主循环在每轮迭代开始时批量消费发送。该机制让 handler 无论运行在哪个线程,都能安全、无锁地发送输出。

表1 SerialEventLoop API功能列表

功能 API 功能 说明
事件分发 register_handler(id, handler) 每个 input_id 绑定一个专属线程 handler
定时器 register_timer(id, interval, cb, repeat) 重复 / 一次性,10ms 精度
取消定时器 cancel_timer(id) 线程安全
输出发送 send_output(id, data) 自动路由(直接发送 / 入队)

2.2 示例代码

事件注册测试代码

cpp 复制代码
#include "SerialEventLoop.hpp"
#include <chrono>

using namespace dora_extensions;

// Test Scenario 1: Simulate a time-consuming image recognition algorithm (200ms)
void on_image_received(const InputEvent& event) {
    std::cout << "[Worker Thread A] ===> START: Processing heavy image task. Size: " 
              << event.data.size() << " bytes." << std::endl;
    
    // Simulate complex OpenCV or deep learning inference execution time
    std::this_thread::sleep_for(std::chrono::milliseconds(200)); 
    
    std::cout << "[Worker Thread A] <=== END: Image processing done." << std::endl;
}

// Test Scenario 2: Simulate an ultra-lightweight, high-frequency/periodic control command processing (0ms)
void on_tick_received(const InputEvent& event) {
    // Safely convert the binary byte stream back to a string for printing (assuming text format)
    std::string msg(reinterpret_cast<const char*>(event.data.data()), event.data.size());
    
    std::cout << "[Worker Thread B] Fast reply for topic [" << event.id 
              << "] -> Value: " << msg << std::endl;
}

int main() {
    // 1. Create an instance of the event loop, named "my_test_node"
    SerialEventLoop loop("my_test_node");

    // 2. Register the binding relationship between topics and callback functions
    // When "image_task" is triggered, data is automatically dispatched to Worker Thread A's private queue
    loop.register_handler("image_task", on_image_received);
    
    // When "tick" is triggered, data is automatically dispatched to Worker Thread B's private queue
    loop.register_handler("tick", on_tick_received);

    // 3. Block and run. The main thread takes over the native C API polling of the Dora runtime
    loop.run();

    return 0;
}

SerialEventLoop.hpp 源代码

cpp 复制代码
#include "SerialEventLoop.hpp"
#include <cassert>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

using namespace dora_extensions;

// ============================================================
// 测试1: SerialWorker 独立单元测试
// ============================================================
void test_serial_worker_basic() {
    std::cout << "[测试1] SerialWorker 基本功能..." << std::endl;

    std::atomic<int> call_count{0};
    std::vector<std::string> received_ids;

    {
        SerialWorker worker("test_worker",
            [&](const InputEvent& e) {
                call_count++;
                received_ids.push_back(e.id);
            });

        // 入队几个事件
        for (int i = 0; i < 5; i++) {
            InputEvent ie;
            ie.id = "input_" + std::to_string(i);
            ie.data = {uint8_t(i), uint8_t(i + 1), uint8_t(i + 2)};
            worker.enqueue(ie);
        }

        // 等待worker处理完(给一点时间)
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    } // worker析构,自动stop

    assert(call_count == 5);
    assert(received_ids.size() == 5);
    for (int i = 0; i < 5; i++) {
        assert(received_ids[i] == "input_" + std::to_string(i));
    }

    std::cout << "[测试1] 通过 ✓" << std::endl;
}

// ============================================================
// 测试2: SerialWorker 顺序性测试
// ============================================================
void test_serial_worker_ordering() {
    std::cout << "[测试2] SerialWorker 事件顺序性..." << std::endl;

    std::vector<int> processed;
    std::mutex mtx;

    SerialWorker worker("order_worker",
        [&](const InputEvent& e) {
            std::lock_guard<std::mutex> lock(mtx);
            processed.push_back(e.data[0]);
        });

    // 按顺序入队100个事件
    for (int i = 0; i < 100; i++) {
        InputEvent ie;
        ie.id = "seq";
        ie.data = {uint8_t(i)};
        worker.enqueue(ie);
    }

    // 等待全部处理完
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    assert(processed.size() == 100);
    for (int i = 0; i < 100; i++) {
        assert(processed[i] == i); // 必须严格保序
    }

    std::cout << "[测试2] 通过 ✓ (处理了 " << processed.size() << " 个事件)" << std::endl;
}

// ============================================================
// 测试3: SerialEventLoop 注册与取消
// ============================================================
void test_event_loop_registration() {
    std::cout << "[测试3] EventLoop 注册/取消..." << std::endl;

    SerialEventLoop loop("test_node");

    // 注册handler
    loop.register_handler("camera", [](const InputEvent&) {});
    loop.register_handler("lidar", [](const InputEvent&) {});

    // 注册定时器
    int tick_count = 0;
    loop.register_timer("tick", std::chrono::milliseconds(50),
        [&]() { tick_count++; }, true);

    // 等待定时器触发几次
    std::this_thread::sleep_for(std::chrono::milliseconds(120));
    assert(tick_count >= 2);

    // 取消定时器
    bool ok = loop.cancel_timer("tick");
    assert(ok);

    int snapshot = tick_count;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    assert(tick_count == snapshot); // 取消后不应再触发

    // 取消失败的定时器
    ok = loop.cancel_timer("nonexistent");
    assert(!ok);

    std::cout << "[测试3] 通过 ✓ (tick_count=" << tick_count << ")" << std::endl;
}

// ============================================================
// 测试4: 一次性定时器
// ============================================================
void test_oneshot_timer() {
    std::cout << "[测试4] 一次性定时器..." << std::endl;

    SerialEventLoop loop("test_node");
    std::atomic<int> fired{0};

    loop.register_timer("once", std::chrono::milliseconds(30),
        [&]() { fired++; }, false); // repeat=false

    std::this_thread::sleep_for(std::chrono::milliseconds(80));
    // 应该在30ms左右触发一次,之后不再触发
    assert(fired == 1);

    std::this_thread::sleep_for(std::chrono::milliseconds(80));
    assert(fired == 1); // 仍然只有一次

    std::cout << "[测试4] 通过 ✓" << std::endl;
}

// ============================================================
// 测试5: send_output 线程安全
// ============================================================
void test_send_output() {
    std::cout << "[测试5] send_output 入队..." << std::endl;

    SerialEventLoop loop("test_node");

    std::atomic<int> sent{0};

    // 模拟从多个线程调用 send_output
    std::thread t1([&]() {
        for (int i = 0; i < 50; i++) {
            loop.send_output("out_a", {uint8_t(i)});
            sent++;
        }
    });

    std::thread t2([&]() {
        for (int i = 0; i < 50; i++) {
            loop.send_output("out_b", {uint8_t(i)});
            sent++;
        }
    });

    t1.join();
    t2.join();

    assert(sent == 100);
    // 注意: 这个测试不运行 run(),所以消息还没真正发送到Dora
    // 但它们已被安全地放入队列

    std::cout << "[测试5] 通过 ✓ (入队了 " << sent << " 条消息)" << std::endl;
}

// ============================================================
// 测试6: timer回调中调用 send_output
// ============================================================
void test_timer_send_output() {
    std::cout << "[测试6] 定时器回调中发送输出..." << std::endl;

    SerialEventLoop loop("test_node");
    std::atomic<int> output_calls{0};

    // 在timer回调中调用send_output(timer线程非主线程,会入队)
    loop.register_timer("sender", std::chrono::milliseconds(30),
        [&]() {
            loop.send_output("timer_out", {0xAA, 0xBB});
            output_calls++;
        }, true);

    std::this_thread::sleep_for(std::chrono::milliseconds(80));
    assert(output_calls >= 2);

    loop.cancel_timer("sender");
    std::cout << "[测试6] 通过 ✓ (send_output调用了 " << output_calls << " 次)" << std::endl;
}

int main() {
    std::cout << "========================================" << std::endl;
    std::cout << "  SerialEventLoop 单元测试" << std::endl;
    std::cout << "========================================" << std::endl;

    test_serial_worker_basic();
    test_serial_worker_ordering();
    test_event_loop_registration();
    test_oneshot_timer();
    test_send_output();
    test_timer_send_output();

    std::cout << "========================================" << std::endl;
    std::cout << "  全部测试通过 ✓" << std::endl;
    std::cout << "========================================" << std::endl;

    return 0;
}

综上所述,SerialEventLoop的优势,相比于while 循环方式的事件处理框架仅提供了「等事件→处理事件」的最小集;而 SerialEventLoop 代码框架升级为「不同输入源自动隔离到专属线程 + 原生定时器 + 线程安全输出」的完整节点运行时,开发者仅需编写每个 handler 的业务逻辑即可。

相关推荐
技安未来10 个月前
官宣!Dora-rs 官方中文教程正式发布!
dora
技安未来10 个月前
Dora-rs:下一代机器人开发框架
dora
熊猫飞天2 年前
DORA 机器人中间件学习教程(6)——激光点云预处理
dora·dora-rs·机器人中间件·rust框架
熊猫飞天2 年前
DORA 机器人中间件学习教程(5)——3D激光雷达数据可视化
可视化·dora·机器人中间件
Qiming_v2 年前
LoRA 和 DoRA 代码笔记
pytorch·lora·dora
熊猫飞天2 年前
Dora-rs 机器人框架学习教程(3)——利用yolo实现目标检测
yolo·目标检测·dora·dora-rs·机器人框架
熊猫飞天2 年前
Dora-rs 机器人框架学习教程(1)—— Dora-rs安装
教程·dora·dora-rs·机器人框架
平台工程社区2 年前
Google 提示:切忌滥用 DORA 指标
运维·团队开发·devops·dora
Seal软件3 年前
如何正确执行 DORA 指标
devops·持续部署·dora