1.3 线程模型
核心语义
1. 同步调用
Connect()、Read()、MoveJoint()等接口对外均为同步返回。- 调用线程会等待请求完成,再获取
STATUS_CODE或查询结果。 - 同一个
Arm实例上并发发起的网络调用,底层会按提交顺序串行执行。
2. 周期任务
ControllerInfo::AcquireAccess()会注册2000ms的保活定时任务。JoggingControl::ContinuousMove()/MultiMove()会注册50ms的连续点动定时任务。- 这些周期任务复用
Arm会话的专用网络线程。 - 停止
Arm会话时,相关定时任务会一并清理。
3. 订阅发布(SubPub)
TopicPubSub的收包由 WebSocket 库回调进入 SDK,本页不展开 WebSocket 库自己的线程。- SDK 在
SubPubService内维护一条messages消息队列,Receive()从队列中同步获取下一条消息。 - 消息队列上限为
100条,满后会丢弃最旧消息。 - 多个
Arm/TopicPubSub实例可以并发建立各自的 WebSocket 连接;销毁或断开其中一个实例,不应影响其他仍然存活的实例。
SDK 线程视图
线程安全规则
Arm 实例
- 单线程访问:每个
Arm实例应由单一业务线程访问。 - 多实例并发:如需并发控制多台机器人,请创建多个
Arm实例。 - 非线程安全:不在外部同步机制保护下跨线程共享
Arm实例。
服务模块
所有服务模块( motionControl 、 controllerInfo 、 alarmClient 等)继承其所属 Arm 实例的线程安全特性:
cpp
// 安全:单线程访问所有模块 // 同一个业务线程串行调用同一个 Arm
Arm arm; // 创建一个 Arm 会话对象
arm.Connect("192.168.110.2", ""); // 在当前业务线程连接机器人
arm.motionControl.GetCurrentPose(PoseType::JOINT); // 串行读取当前关节位姿
arm.controllerInfo.GetCtrlStatus(); // 串行读取控制状态
arm.alarmClient.GetAllActiveAlarms(); // 串行读取活动报警列表
// 不安全:多线程访问同一 Arm // 下面两行表示反例
// 线程 1 // 第一个线程准备发起运动
arm.motionControl.MoveJoint(target1, 0.5, 0.5); // 线程 1 调用同一个 Arm 的运动接口
// 线程 2(并发) // 第二个线程同时发起运动
arm.motionControl.MoveJoint(target2, 0.5, 0.5); // 未定义行为安全的并发访问模式
cpp
// 安全:多 Arm 实例实现并发访问 // 每台机器人使用独立 Arm 会话
Arm arm1, arm2; // 创建两个互相独立的 Arm 对象
arm1.Connect("192.168.110.2", ""); // 连接第一台机器人
arm2.Connect("192.168.110.3", ""); // 连接第二台机器人
// 线程 1 // 第一个线程只使用 arm1
arm1.motion.MoveJoint(target1, 0.5, 0.5); // 向第一台机器人下发运动
// 线程 2(并发,不同机器人) // 第二个线程只使用 arm2
arm2.motion.MoveJoint(target2, 0.5, 0.5); // 安全回调使用准则
推荐做法
- 回调函数保持简短快速
- 如需后续处理,先复制数据
- 使用线程安全的日志记录
- 快速返回以允许其他通知通过
禁止做法
- 在回调中阻塞
- 在回调中调用阻塞型 SDK API
- 在回调中访问其他
Arm实例(无同步机制) - 在回调中抛出异常
示例:正确的回调模式
cpp
// 正确:快速、非阻塞的回调 // 回调线程只做数据搬运
void OnJointPosition(const JointPositionMsg& msg) { // 收到关节位置消息时进入回调
std::lock_guard<std::mutex> lock(dataMutex_); // 加锁保护共享缓存
latestJoints_ = msg.joints; // 快速复制消息数据
} // 立即返回,避免阻塞接收线程
void ProcessData() { // 在业务线程里处理已经缓存的数据
std::vector<Float64> joints; // 准备业务线程自己的数据副本
{ // 缩小锁作用域
std::lock_guard<std::mutex> lock(dataMutex_); // 加锁读取共享缓存
joints = latestJoints_; // 复制最新关节数据到本地变量
} // 释放锁,让回调线程可以继续更新缓存
// 安全处理数据 // 在锁外执行耗时业务逻辑
} // 结束业务线程处理函数示例:错误的回调模式
cpp
// 错误:在回调中阻塞
void OnAlarm(const AlarmMsg& msg) { // 收到报警消息时进入回调
arm.alarmClient.Reset(); // 禁止在回调里调用阻塞型 SDK API,存在死锁风险
ProcessLargeData(msg); // 禁止在回调里执行耗时处理,会阻塞其他回调
} // 结束错误示例回调推荐使用方式
- 让同一个
Arm的Connect()/Disconnect()/ 模式切换由同一条业务线程或同一处调度逻辑统一管理。 TopicPubSub回调里只做轻量工作,例如解析关键字段、投递到队列、设置标志位。- 耗时逻辑、二次控制指令、重连动作放到业务线程执行,不要直接堵在回调里。
使用约束
禁止的重入操作
不要在 TopicPubSub 回调里直接调用:
Arm::Connect()、Arm::Disconnect()TopicPubSub::Connect()、TopicPubSub::Disconnect()StartReceiving()、RemoveMessageHandler()
这类重入操作可能返回 OTHER_ERR 或被忽略。
其他约束
- 不要假设不同接口模块会并行打到同一台机器人;同一个
Arm的请求默认按串行调度理解更安全。 - 如果业务需要高吞吐或隔离不同设备,请使用多个
Arm/ArmHandle*会话。
C99 说明
Arm_Destroy()前应先Arm_Disconnect(),确保对应会话的网络线程和定时任务被正常清理。
性能考量
连接恢复
如果网络线程检测到断开连接:
- 连接恢复由调用方执行:先调用
Disconnect(),再调用Connect() - 所有待处理操作返回错误码
最佳实践总结
- 数据复制:在回调中复制数据,在工作线程处理
- 错误处理:始终检查返回码,优雅处理断连
- 资源清理:销毁
Arm实例前务必调用Disconnect()