并发请求与回调匹配
多线程调用 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上「一条线程收、另一条线程发」是否安全,文档未明确([待核]);保守做法是收发都放在同一工作线程。