Skip to content
🤖 AI 助手 / 自动化工具:先读 《AI 助手先读》 《Agent 开发指引》 《生成前信息采集清单》;机器可读入口 /llms.txt;技能包(SKILL)直链:SKILL.md

并发请求与回调匹配 ​

多线程调用 SDK、或者在回调里再调 SDK,是最容易出「串包」「偶发崩溃」「结果对不上」的地方。本页说明 SDK 的线程边界,以及推荐的三种组织方式。

一、SDK 的线程边界 ​

规则说明
不同 socketFd 之间线程安全每个连接独立收发,可以各用一个线程
同一个 socketFd 的并发调用需自行加锁两条线程同时对同一连接发命令,收发可能错位
回调函数内不得阻塞或耗时回调在 SDK 的接收线程里执行,阻塞会拖住整条连接
回调不带「请求 ID」无法用回调去对应某一次具体调用,见第三节

二、三种组织方式 ​

方式适用代价
A. 一连接一线程(推荐)多机器人、多端口(6001 + 7000)连接数变多,管理稍复杂
B. 共享连接 + 互斥锁单机器人、调用不频繁(点位、参数、IO)锁粒度大时容易卡界面
C. 命令队列串行化(推荐给带界面的程序)UI 线程 + 后台工作线程需要自己写一个任务队列

为什么推荐 C

SDK 的调用大多是「发命令 → 等应答」的同步阻塞。放在 UI 线程里直接调用,网络一抖界面就假死。把调用丢进队列、由单一工作线程顺序执行,既不违反「同一 fd 需串行」的约束,也不阻塞界面。

三、回调和请求怎么对上 ​

回调携带信息能用来做什么
recv_message(fd, cb(int messageID, const char* message))消息 ID + 内容按 messageID 分类处理(消息 ID 的含义见消息表)
set_receive_error_or_warnning_message_callback(fd, cb(int messageType, const char* message, int messageCode))消息类型 + 内容 + 消息码按 messageCode 匹配错误,做报警提示
状态回调(7000 侧)状态值刷新界面状态,不要用来判定某次运动的完成

关键点:这些回调都不是「某次调用的应答」。所以:

  • 判断成功与否,一律看接口返回值;
  • 判断运动是否完成,看状态 / 位置回读,不要等回调;
  • 需要「这条命令对应哪个业务对象」,自己在业务层维护映射(例如命令序号 + 时间戳),不要指望回调带上下文。

四、反例(现场踩过的) ​

反例后果
两个线程同时对同一 fd 调 set_digital_output一个成功一个超时,或写错端口
在错误回调里调用 clear_error 或 set_servo_state回调线程被阻塞 → 收包停止 → 整条连接假死
用状态回调的触发次数去数「走了几个点」回调频率与运动进度无关,数出来永远对不上
UI 线程直接调 connect_robot连不上时界面卡死,用户以为程序崩了

五、示例:命令队列 + 单工作线程 ​

cpp
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>
#include "cpp/interface/nrc_api.h"

class CommandQueue
{
public:
    explicit CommandQueue(SOCKETFD fd) : fd_(fd), stop_(false)
    {
        worker_ = std::thread([this] { run(); });
    }

    ~CommandQueue()
    {
        {
            std::lock_guard<std::mutex> lk(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
        worker_.join();
    }

    // 任意线程都可以投递;回调里只投递、不执行
    void post(std::function<void(SOCKETFD)> job)
    {
        {
            std::lock_guard<std::mutex> lk(mtx_);
            jobs_.push(std::move(job));
        }
        cv_.notify_one();
    }

private:
    void run()
    {
        while (true) {
            std::function<void(SOCKETFD)> job;
            {
                std::unique_lock<std::mutex> lk(mtx_);
                cv_.wait(lk, [this] { return stop_ || !jobs_.empty(); });
                if (stop_ && jobs_.empty()) { return; }
                job = std::move(jobs_.front());
                jobs_.pop();
            }
            job(fd_);   // 同一时刻只有一个线程用这个 fd
        }
    }

    SOCKETFD fd_;
    std::mutex mtx_;
    std::condition_variable cv_;
    std::queue<std::function<void(SOCKETFD)>> jobs_;
    std::thread worker_;
    bool stop_;
};

用法:UI 线程把要执行的调用投递进去,工作线程顺序执行,天然满足「同一 fd 串行」。

cpp
queue.post([](SOCKETFD fd) {
    set_digital_output(fd, 3, 1);
});

六、待核 ​

  • 状态回调(7000 侧)的确切函数签名与回调线程模型,以包内头文件为准。
  • 同一 socketFd 上「一条线程收、另一条线程发」是否安全,文档未明确([待核]);保守做法是收发都放在同一工作线程。