Skip to content

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 实例。

服务模块

所有服务模块( motionControlcontrollerInfoalarmClient 等)继承其所属 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);  // 禁止在回调里执行耗时处理,会阻塞其他回调
}  // 结束错误示例回调

推荐使用方式

  • 让同一个 ArmConnect() / 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()
  • 所有待处理操作返回错误码

最佳实践总结

  1. 数据复制:在回调中复制数据,在工作线程处理
  2. 错误处理:始终检查返回码,优雅处理断连
  3. 资源清理:销毁 Arm 实例前务必调用 Disconnect()