2026 ROS 2 Lyrical C++ 入门(六):为什么节点会“卡住”?——Executor、回调组与死锁排查

VOWIFI--相关的协议文档 一、3GPP相关协议文档 3GPP 23.402 Architecture enhancements for non-3GPP accesse- //重要 整体框架结构和流程,3GPP和NON-3GPP切换 3GPP 36.304  User Equipment (UE) procedures in idle mode //重要,终端在空闲状态下3GPP与NON-3GPP之间的重选方法   3GP 阅读详情

你可能遇到过这样的情况:Service 服务端明明已经处理请求,客户端却一直等不到结果;Action 已经开始执行,取消请求却迟迟没有反应。

第一反应往往是网络、DDS 或 QoS。于是增加超时时间,甚至把单线程换成多线程。

结果还是一样。

问题有时不在消息有没有到,而在于:处理消息的代码,是否有机会执行?

上一篇我们用定时器推进 Action 任务,避免在回调里写长循环。这一篇继续往下挖:Executor、线程与回调组分别限制了什么,为什么“用了异步接口”仍然可能把自己等住?

实测环境:2026 年 9 月 20 日,在 WSL2 / Ubuntu 26.04 LTS、ROS 2 Lyrical、rclcpp 32.0.2、GCC 15.2.0、C++17 下完成编译,并通过本文全部 8 组对照实验。运行使用 Cyclone DDS(rmw_cyclonedds_cpp)、ROS_DOMAIN_ID=187、本机发现模式。具体时序受机器负载影响,不能将本次间隔数值视为实时性保证。

示例故意制造阻塞,但等待有上限,不控制真实机器人。请勿直接放入底盘控制或机械臂安全链路。

一、先区分三种“卡住”

“节点卡住”是现象,不是诊断。

现象可能原因本文怎样验证
某个回调很久才执行唯一线程正忙于另一个回调耗时回调与心跳定时器
已换成多线程,仍然串行两个回调位于同一个互斥组保持线程数不变,只修改分组
回调等待结果,结果又无法被处理等待者占用了被等待处理所需的资源Service 请求与响应对照

第一种不一定是死锁:耗时工作结束,其他回调可能继续。

第三种如果无限等待,就可能形成无法自行解除的循环依赖。本文用 wait_for(3s) 给它一个退出条件,因此实际示例会超时返回,不能把它描述为“永久死锁”。

二、线程数量与回调组,管的不是同一件事

Executor 负责驱动可执行的工作,例如定时器回调、订阅回调和客户端响应处理。

SingleThreadedExecutor 一次只执行一项回调工作。MultiThreadedExecutor 提供多个工作线程,但“有空闲线程”并不等于“某个回调被允许同时执行”。

回调组还会施加约束:

  • MutuallyExclusive:同组回调不能同时执行。
  • Reentrant:允许同组并发,适用时也允许同一个回调的不同执行实例重叠。
  • 不同回调组可以并行,但还要有可用线程,且不能被业务锁等条件挡住。

没有显式指定时,节点的默认回调组是互斥组。因此,把节点直接放进多线程 Executor,并不会自动让默认组中的回调并发。

这些约束可对照 Lyrical 官方回调组指南。

下面不继续堆概念,直接观察同一份代码在不同配置下的行为。

三、准备一个独立实验包

本篇不依赖上一篇的自定义 Action,用两个定时器和标准 Trigger 服务就能说明问题。

在工作空间中建立目录:

source /opt/ros/lyrical/setup.bash
mkdir -p ~/ros2_lyrical_ws/src/lyrical_executor_demo/src
cd ~/ros2_lyrical_ws/src/lyrical_executor_demo
lyrical_executor_demo/
├── CMakeLists.txt
├── package.xml
└── src/
    ├── scheduling_demo.cpp
    ├── ping_server.cpp
    └── wait_client.cpp

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(lyrical_executor_demo LANGUAGES CXX)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_srvs REQUIRED)
find_package(Threads REQUIRED)
foreach(target scheduling_demo ping_server wait_client)
  add_executable(${target} src/${target}.cpp)
  target_compile_features(${target} PRIVATE cxx_std_17)
  target_link_libraries(${target} PRIVATE
    rclcpp::rclcpp std_srvs::std_srvs Threads::Threads)
endforeach()
install(TARGETS scheduling_demo ping_server wait_client
  DESTINATION lib/${PROJECT_NAME})
ament_package()

package.xml

<?xml version="1.0"?>
<package format="3">
  <name>lyrical_executor_demo</name>
  <version>0.1.0</version>
  <description>Executor and callback group experiments</description>
  <maintainer email="maintainer@example.com">Tutorial Maintainer</maintainer>
  <license>Apache-2.0</license>
  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>rclcpp</depend>
  <depend>std_srvs</depend>
  <export><build_type>ament_cmake</build_type></export>
</package>

本文继续使用 target_link_libraries() 和导出的依赖 target,不调用 ament_target_dependencies()。Threads::Threads 用于显式链接标准线程示例需要的线程库。

四、实验一:一个耗时回调,让心跳停顿

保存为 src/scheduling_demo.cpp:

#include <chrono>
#include <memory>
#include <stdexcept>
#include <thread>
#include "rclcpp/rclcpp.hpp"

using namespace std::chrono_literals;
using Clock = std::chrono::steady_clock;

class SchedulingDemo : public rclcpp::Node
{
public:
  SchedulingDemo() : Node("scheduling_demo")
  {
    threads = declare_parameter<int>("threads", 1);
    const bool split = declare_parameter<bool>("split_groups", false);
    if (threads < 1 || threads > 8) {
      throw std::runtime_error("threads must be in [1, 8]");
    }
    if (split) {
      slow_group_ = create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
      fast_group_ = create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
    }
    last_ = Clock::now();
    heartbeat_ = create_wall_timer(200ms, [this]() {
      const auto now = Clock::now();
      const auto gap = std::chrono::duration<double, std::milli>(now - last_).count();
      last_ = now;
      RCLCPP_INFO(get_logger(), "heartbeat gap=%.0f ms", gap);
    }, fast_group_);
    slow_ = create_wall_timer(1s, [this]() {
      slow_->cancel();  // Run the slow operation only once.
      RCLCPP_INFO(get_logger(), "slow BEGIN");
      std::this_thread::sleep_for(2s);  // Deliberately bad for this experiment.
      RCLCPP_INFO(get_logger(), "slow END");
    }, slow_group_);
  }
  int threads{1};

private:
  Clock::time_point last_;
  rclcpp::CallbackGroup::SharedPtr slow_group_, fast_group_;
  rclcpp::TimerBase::SharedPtr heartbeat_, slow_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<SchedulingDemo>();
  if (node->threads == 1) {
    rclcpp::executors::SingleThreadedExecutor executor;
    executor.add_node(node);
    executor.spin();
  } else {
    rclcpp::executors::MultiThreadedExecutor executor(
      rclcpp::ExecutorOptions(), static_cast<size_t>(node->threads));
    executor.add_node(node);
    executor.spin();
  }
  rclcpp::shutdown();
  return 0;
}

程序有两个定时器:心跳期望每 200 毫秒执行一次,慢回调在启动后约 1 秒触发,只执行一次,并故意 sleep 2 秒。

heartbeat gap 使用 steady_clock 测量相邻心跳回调的实际执行间隔,而不是使用可能随仿真暂停的 ROS 时间。重点观察 slow BEGIN 到 slow END 之间,心跳能否继续。

注意,这里 sleep 的目的就是制造坏例子,不是建议在控制回调里这样写。

后面的两个源码文件也保存完成后,再统一构建。此处先列出本实验的运行方式。

4.1 单线程、默认组

ros2 run lyrical_executor_demo scheduling_demo --ros-args \
  -p threads:=1 -p split_groups:=false

预期:开始时心跳间隔接近 200 毫秒;慢回调执行期间心跳出现明显空档;慢回调返回后恢复。

间隔不保证精确为 200 或 2000 毫秒。操作系统调度、日志输出和定时器相位都会影响结果;不要把示例当成实时性基准测试。

虽然 sleep 期间 CPU 可以运行其他线程,但唯一的 Executor 执行线程并没有从当前回调返回,不能继续处理这个节点的心跳。

4.2 已拆组,但仍是单线程

ros2 run lyrical_executor_demo scheduling_demo --ros-args \
  -p threads:=1 -p split_groups:=true

预期仍然有心跳停顿。分组只是允许并发,不会替你创建额外执行线程。

每次测试后按 Ctrl+C 退出,再运行下一项,避免多个同名节点混淆日志。

五、实验二:两条线程,为什么还是没有改善?

5.1 两线程、默认组

ros2 run lyrical_executor_demo scheduling_demo --ros-args \
  -p threads:=2 -p split_groups:=false

预期仍然出现慢回调期间的心跳空档。

线程增加了,但两个定时器仍然使用同一个默认互斥组。慢回调尚未结束时,心跳不能进入该组执行。

这解释了一种常见误区:MultiThreadedExecutor 是提供执行资源,不是解除互斥规则。

5.2 两线程、不同组

ros2 run lyrical_executor_demo scheduling_demo --ros-args \
  -p threads:=2 -p split_groups:=true

预期心跳可以在 slow BEGIN 与 slow END 之间继续出现。

slow_group_ 和 fast_group_ 都是互斥组,因此各自组内仍然串行;两个组之间则允许并行。我们没有为了并发把所有回调都改成 Reentrant。

线程与分组慢回调执行时的预期心跳
单线程,同组明显停顿
单线程,不同组明显停顿
两线程,同组明显停顿
两线程,不同组可以继续执行,但不保证精确周期

回调组保存在成员变量里,避免生命周期过早结束。定时器的 group 参数写法可核对 Lyrical Node API。

六、实验三:服务端已经处理了,客户端为什么超时?

这个实验将服务端放在另一个进程,避免把“服务端回调被本地线程卡住”和“客户端响应无法处理”混在一起。

6.1 一个快速返回的服务端

保存为 src/ping_server.cpp:

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/trigger.hpp"

using Trigger = std_srvs::srv::Trigger;

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<rclcpp::Node>("ping_server");
  auto service = node->create_service<Trigger>(
    "executor_ping",
    [node](const std::shared_ptr<Trigger::Request>,
      std::shared_ptr<Trigger::Response> response)
    {
      response->success = true;
      response->message = "pong";
      RCLCPP_INFO(node->get_logger(),
        "SERVER callback prepared response; returning");
    });
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

服务端不模拟任何耗时,只填写响应并返回。日志刻意写成“prepared response; returning”,而不是“客户端已经收到”:服务端回调准备好响应,不等于客户端已经处理响应。

6.2 一个可以切换等待方式的客户端

保存为 src/wait_client.cpp:

#include <chrono>
#include <cstdint>
#include <future>
#include <memory>
#include <stdexcept>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/trigger.hpp"

using namespace std::chrono_literals;
using Trigger = std_srvs::srv::Trigger;
using Client = rclcpp::Client<Trigger>;
using Clock = std::chrono::steady_clock;

class WaitClient : public rclcpp::Node
{
public:
  WaitClient() : Node("wait_client")
  {
    threads = declare_parameter<int>("threads", 2);
    mode_ = declare_parameter<std::string>("mode", "wait");
    const bool split = declare_parameter<bool>("split_groups", false);
    if (threads < 1 || threads > 8 || (mode_ != "wait" && mode_ != "async")) {
      throw std::runtime_error("Invalid threads or mode");
    }
    // Keep async state serialized in the default mutually-exclusive group.
    if (mode_ == "async" && split) {
      throw std::runtime_error("Use split_groups:=false for the async experiment");
    }
    if (split) {
      client_group_ = create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
    }
    client_ = create_client<Trigger>(
      "executor_ping", rclcpp::ServicesQoS(), client_group_);

    trigger_ = create_wall_timer(500ms, [this]() { send_once(); });
    watchdog_ = create_wall_timer(100ms, [this]() {
      if (pending_ && Clock::now() >= deadline_) {
        client_->remove_pending_request(request_id_);
        pending_ = false;
        RCLCPP_WARN(get_logger(),
          "ASYNC TIMEOUT: local request tracking removed, server NOT canceled");
      }
    });
  }

  bool server_ready() { return client_->wait_for_service(5s); }
  int threads{2};

private:
  void send_once()
  {
    trigger_->cancel();
    auto request = std::make_shared<Trigger::Request>();
    RCLCPP_INFO(get_logger(), "CLIENT sending, mode=%s", mode_.c_str());

    if (mode_ == "async") {
      const auto handle = client_->async_send_request(
        request, [this](Client::SharedFuture response) {
          pending_ = false;
          const auto result = response.get();  // Already ready inside this callback.
          RCLCPP_INFO(get_logger(), "ASYNC RESPONSE success=%d message=%s",
            static_cast<int>(result->success), result->message.c_str());
        });
      request_id_ = handle.request_id;
      deadline_ = Clock::now() + 3s;
      pending_ = true;
      RCLCPP_INFO(get_logger(), "CLIENT callback returning without waiting");
      return;
    }

    auto future = client_->async_send_request(request);
    RCLCPP_INFO(get_logger(), "CLIENT waiting inside timer callback");
    if (future.wait_for(3s) == std::future_status::ready) {
      const auto response = future.get();
      RCLCPP_INFO(get_logger(), "WAIT RESPONSE success=%d message=%s",
        static_cast<int>(response->success), response->message.c_str());
    } else {
      client_->remove_pending_request(future);
      RCLCPP_WARN(get_logger(),
        "WAIT TIMEOUT: timer is only now allowed to return");
    }
  }

  std::string mode_;
  rclcpp::CallbackGroup::SharedPtr client_group_;
  Client::SharedPtr client_;
  rclcpp::TimerBase::SharedPtr trigger_, watchdog_;
  bool pending_{false};
  int64_t request_id_{0};
  Clock::time_point deadline_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<WaitClient>();
  // No executor is spinning yet; this does not occupy an executor callback.
  if (!node->server_ready()) {
    RCLCPP_ERROR(node->get_logger(), "Server unavailable; start ping_server first");
    rclcpp::shutdown();
    return 2;
  }
  if (node->threads == 1) {
    rclcpp::executors::SingleThreadedExecutor executor;
    executor.add_node(node);
    executor.spin();
  } else {
    rclcpp::executors::MultiThreadedExecutor executor(
      rclcpp::ExecutorOptions(), static_cast<size_t>(node->threads));
    executor.add_node(node);
    executor.spin();
  }
  rclcpp::shutdown();
  return 0;
}

现在三个源码文件齐全,可以构建:

source /opt/ros/lyrical/setup.bash
cd ~/ros2_lyrical_ws
rosdep install --from-paths src --ignore-src -r -y --rosdistro lyrical
colcon build --symlink-install --packages-select lyrical_executor_demo
source install/setup.bash

每个运行终端都加载:

source /opt/ros/lyrical/setup.bash
source ~/ros2_lyrical_ws/install/setup.bash

为复现本文实测配置,请在每个运行终端继续设置:

export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export ROS_DOMAIN_ID=187
export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST

如果未安装 Cyclone DDS,可先执行 sudo apt install ros-lyrical-rmw-cyclonedds-cpp。所有实验节点应使用一致的配置。本机发现模式仅用于本文的同机实验,不适用于跨机器部署。

本次首次使用默认 Fast DDS 与本机发现配置时,客户端未发现服务端,尚未进入请求实验;切换为上述配置后全部通过。这里记录的是当前环境的观察,不代表 Fast DDS 普遍不可用,也不能把服务发现失败当作回调组死锁。

终端 A 保持服务端运行:

ros2 run lyrical_executor_demo ping_server

终端 B 运行客户端:

ros2 run lyrical_executor_demo wait_client --ros-args \
  -p threads:=2 -p split_groups:=false -p mode:=wait

预期服务端很快打印准备响应的日志,客户端却在约 3 秒后报告 WAIT TIMEOUT。

为什么 async_send_request() 没有救它?

因为代码紧接着调用 future.wait_for(3s),又在当前回调里把执行流堵住了。请求发送方式是异步的,等待方式却重新变成了阻塞。

本例的依赖关系是:

定时器回调占用默认互斥组
    ↓ 等待
Future 就绪
    ↓ 需要
Executor 执行客户端响应处理
    ↓ 需要
进入同一个默认互斥组
    ↓ 但该组正被等待中的定时器回调占用

服务端快不快、客户端线程多不多,都不能直接解除这个组内循环依赖。

3 秒到期后,回调终于返回,组才重新可用。本例也会移除本地 pending request,避免放弃等待后仍无限积累请求记录;晚到的响应不会再交给这一条已经移除的请求。

如果把有限等待改成无限 wait() 或对尚未就绪的 Future 直接 get(),在这些条件下就可能永久等住。本文不要求读者实际制造无限等待。

七、修复方案一:给响应处理让出组和线程

保留 wait 模式,只改分组:

ros2 run lyrical_executor_demo wait_client --ros-args \
  -p threads:=2 -p split_groups:=true -p mode:=wait

预期客户端可以收到 WAIT RESPONSE,而不必等到 3 秒上限。

原因是定时器仍在默认组,客户端响应处理放进另一个互斥组;第二条线程能够处理响应,使 Future 就绪,让第一条线程结束等待。

再做一个反向实验:

ros2 run lyrical_executor_demo wait_client --ros-args \
  -p threads:=1 -p split_groups:=true -p mode:=wait

预期又会超时。虽然组分开了,唯一线程仍然在等待,没有其他线程处理响应。

这是一种可用修复,但不是无限扩展方案。如果很多回调都同步等待,有限的线程池仍可能被占满;跨组回调如果拿着同一把业务锁,也可能互相卡住。

因此,不能把“分成两个组”理解成任何阻塞程序的通用免死金牌。

八、修复方案二:发出请求后返回,让响应来继续工作

更适合这个例子的方式,是 async 模式:

ros2 run lyrical_executor_demo wait_client --ros-args \
  -p threads:=1 -p split_groups:=false -p mode:=async

预期即使单线程、同一个默认互斥组,也能收到 ASYNC RESPONSE。

区别只有一处,却非常重要:定时器发出请求后立即返回,不再占着回调执行位置等结果。

程序随后在响应回调里读取已经就绪的 Future,继续处理业务。不是所有 get() 都会阻塞:关键是调用时 Future 是否已经就绪。

这个模式也有 3 秒等待上限,只不过由另一个短定时器检查,而不是让发请求的回调睡在那里。超过期限时调用 remove_pending_request() 清理本地状态。

相关 API 与清理要求可对照 Lyrical rclcpp Client 实现与说明。

这里有三个边界:

  1. 本例 async 模式强制使用默认互斥组,使响应回调、发送回调和超时检查串行,保护 pending_ 等状态。故意组合 async 与 split_groups:=true 会报参数错误,不能直接放开后宣称线程安全。
  2. 100 毫秒检查一次超时不意味着精确截止;如果 Executor 被别的耗时工作占用,超时检查也会延迟。它不是独立安全看门狗。
  3. remove_pending_request() 只是移除客户端等待记录,不是取消服务端正在执行的工作。Service 没有因此获得 Action 的取消语义。

客户端只发送一次请求,观察到结果或超时后仍保持 spin,请按 Ctrl+C 退出。这样方便查看日志,不需要抢读一闪而过的窗口。

九、把这些结论接回上一篇 Action

上一篇的服务端使用定时器分步推进,所以处理一次计数后就返回,Executor 有机会处理取消请求。

如果你将整个搬运过程改成同组回调里的长循环,取消请求即使已经在等待处理,也可能无法及时进入业务逻辑。

客户端也有类似问题:在某个回调里发出目标后,同步等待最终结果,若响应处理需要同组执行机会,就可能再次形成等待关系。

但不要把 Service 的示例机械当成所有 Action 问题的唯一原因。Action 还要检查目标是否接受、服务端是否产生终态、取消是否被执行,以及系统是否失联。

建议先区分:

  • 请求是否进入服务端;
  • 服务端有没有准备好响应或结果;
  • 客户端的处理代码有没有机会执行;
  • 收到的是阶段响应,还是最终结果。

十、几个“修好了,又埋坑”的操作

10.1 把所有回调组都改成 Reentrant

这可能允许并发,也可能把共享状态暴露给多个执行线程。互斥组保护的是该组内回调的执行关系;一旦跨组或允许重入,任务变量、容器和设备句柄就需要重新审查。

本例的 last_ 只有心跳回调访问,两个独立互斥组不会让两个心跳实例同时修改它。因此并不是“有多线程就所有成员都必须加锁”,而是要看谁实际共享了什么。

10.2 在回调里再次 spin_until_future_complete()

上一篇客户端从 main 的普通流程中使用 spin_until_future_complete(),与在已经由 Executor 驱动的回调里嵌套 spin 不同。

不要为了等待一个响应,把同一个节点再次交给另一个 Executor。重复关联或递归 spin 可能触发运行时错误,也不是解决互斥组依赖的正确方式。优先采用响应回调延续业务。

10.3 把超时从 3 秒改成 30 秒

如果问题是循环等待,改大超时只会让空等更久。要先确认缺的是时间,还是执行机会。

10.4 以为 sleep 会释放回调组

sleep 可以让出 CPU,但不会让当前回调返回。线程和该回调占用的组仍然没有完成这次执行。

10.5 看见服务端日志,就宣布 DDS 没问题

日志只是证据的一部分。服务端回调准备响应,不等于传输和客户端处理都成功。本文依靠对照实验定位组约束;真实项目仍需结合发现、类型、域、通信和日志进一步判断。

十一、8 组实测结果与复现清单

实验参数组合本次实测结果(全部通过)
心跳 Athreads=1,split=false慢回调期间停顿
心跳 Bthreads=1,split=true仍停顿
心跳 Cthreads=2,split=false仍停顿
心跳 Dthreads=2,split=true慢回调期间继续心跳
请求 Athreads=2,split=false,mode=waitWAIT TIMEOUT
请求 Bthreads=2,split=true,mode=waitWAIT RESPONSE
请求 Cthreads=1,split=true,mode=waitWAIT TIMEOUT
请求 Dthreads=1,split=false,mode=asyncASYNC RESPONSE

这里的 split 是表格缩写,实际参数名为 split_groups。运行前确认 ping_server 可用,每项只运行一个客户端,测试后退出,再开始下一项。

本次心跳 A、B、C 的最大间隔分别为 2200、2000、2200 毫秒,慢回调执行期间均没有心跳日志;心跳 D 在慢回调期间记录到 10 次心跳,最大间隔为 200 毫秒。

请求 A 的实际日志节选如下,时间戳保留,约 3 秒后退出等待:

[INFO] [1789900555.174585276] [wait_client]: CLIENT sending, mode=wait
[INFO] [1789900555.174725979] [wait_client]: CLIENT waiting inside timer callback
[WARN] [1789900558.174891246] [wait_client]: WAIT TIMEOUT: timer is only now allowed to return

其余请求实验分别记录到 WAIT RESPONSE、WAIT TIMEOUT、ASYNC RESPONSE。所有测试进程采集完成后由测试脚本发送 SIGINT 正常结束,退出码均为 0。构建成功及这 8 组结果说明本文实验在上述配置中得到复现,不代表覆盖所有中间件、负载和异常场景。

如果你的输出不同,应先保留完整日志、参数和版本,而不是为了让现象“符合教程”继续增加 sleep。

总结:先画清谁在等谁,再决定加多少线程

节点没有响应,不一定是通信失败;多线程没有改善,也不一定是线程数太少。

排查这类问题,先看三个条件:

谁占着执行线程?谁占着互斥组?它等待的结果,又需要谁来处理?

如果等待者占用了完成等待所必需的资源,就应该修改执行结构,而不是只延长超时。

对于本文的请求场景,最清晰的改法是:发请求后返回,让 Executor 继续工作,在响应回调中处理结果。确实需要阻塞等待时,则必须同时确认线程、回调组、锁和退出条件。

把这个关系想清楚,Service 的“收不到响应”和 Action 的“取消没反应”,才有机会从猜测变成可验证的排查。

知识图谱-Neo4j-开始构建知识图谱-01 关系(Relationships):连接两个节点,具有方向(→ 或 ←)和类型(Type),可附加属性。 属性(Properties):键值对,存储节点或关系的详细信息(如 , )。 (2) Cypher 查询语言 直观语法:通过模式匹配直接描述图结构。 高效遍历:支持复杂关系路径查询(如最短路径、模式匹配)。 3. 核心优势 (1) 原生图存储 物理存储优化:数据以图结构直接存储,避免传统数据库的“表连接”开销。 高效遍历:通过指针直接访问相邻节点,时间复杂度接近 O(1)。 (2) 高性 阅读详情

相关推荐

用ST25DV芯片+KiCad快速设计NFC标签天线:参数计算/脚本生成/实测优化全流程

本文详细介绍了使用ST25DV芯片和KiCad工具设计NFC标签天线的全流程,包括参数计算、脚本生成和实测优化。通过解析芯片等效工作原理和设计思路,帮助开发者避开常见陷阱,实现高效、可量产的天线设计。文章还提供了Python计算工具和KiCad自动化脚本,大幅提升设计效率和成功率。

jj890的博客 758

把 Agent 搬进飞书:飞书 Agent 机器人演示

本文展示下如何将 Agent 搬进飞书,协助提高日常的工作效率。

分享 Go、云原生、AI Infra 相关技术 297

【自动驾驶】汽车智能驾驶计算芯片

本文介绍汽车智能驾驶计算芯片的软硬件要求、 可靠性和安全性要求、 性能要求,和相应的试验方法。除了用于汽车智能驾驶, 其他领域的计算芯片可参照执行。

Mr.Cssust的博客 2631

从单机到柔性岛:工业连接器如何让机器人真正实现“一机多用”?

如今,一台机器人往往不只承担一道工序。搬运、焊接、涂胶、装配,末端工具在不同工序间快速切换,正在把传统的“专机+专用工位”带向更紧凑的柔性岛。但现场很快会发现:让机器人把工具取下来、再装上去,并不算难;真正难的是,工具切换后,动力、控制、反馈和介质能否同步、稳定地到位。一次连接如果不可靠,轻则需要人工复位,重则影响整段工艺节拍。对于自动化集成商和机器人应用端而言,工业连接器已不再是被动附件,而是把“工具可换”转化为“工艺可切”的基础接口。

SHQT888888的博客 330

STM32CubeMx搭建LWIP程序

  使用STM32CubeMx5.21,在stm32f745平台上搭建ethernet通信程序,PHY选用KSZ8081RNB。(LAN8742/DP83848使用一样) 1、系统时钟设置   RCC设置,勾选外部高速时钟源。   系统时钟配置。写入外部晶振频率、HSE提供PLCCLK、HCLK,其他参数自动生成。 2、ETH设置   勾选设置NVIC。   根据硬件设置对应GPIO引脚。   参数设置默认   根据硬件设置PHY地址,其他参数默认。 3、LWIP设置   勾选使能,通用参数设置

Gandalf94的博客 3239

TCGA之GDC-client工具下载安装与使用

文章主要是对GDC-client工具下载TCGA数据库数据整个过程的一个记录 1.下载GDC-client工具 GDC-client工具网址:GDC Data Transfer Tool | NCI Genomic Data Commons (cancer.gov) 我这里下的是上图中红圈中的版本, 2.解压安装 从网上了解的是不能解压到带中文的路径里,解压后是一个.exe文件,为了能直接在cmd命令行下使用,将其加入系统环境: 点击新建之后,输入将文件解压到的路径,然后...

better_strong的博客 1万+

环视同步与拼接的工程改法:从车规到机器人

重叠区的形状也要看:横向窄、纵向长的重叠,和四四方方的重叠,能提供的约束不一样,前者在俯仰变化时更脆弱。务实做法是给相机支架足够的刚性,把易变的安装点做成可重复定位的结构,再配一套快速校验手段。支架刚性也不是越硬越好,而是要在刚度、重量、成本之间取平衡:机器人末端对重量敏感,一味加厚支架会影响运动性能,这时更该把可重复定位和快速校验做扎实。动态范围的处理没有银弹。拼接错了,标定也救不回来:重叠区不足时,匹配点不够,任何算法都只能猜。相机装在哪里,要看机器人在典型任务中的姿态,而不是看它在展厅里站着的样子。

gunser的博客 222

【eNSP 华为模拟器】单臂路由实战:从原理到配置全解析

本文详细解析了单臂路由技术,通过华为eNSP模拟器从原理到配置进行实战教学。文章阐述了单臂路由如何利用路由器单物理接口创建多个子接口,实现不同VLAN间通信,并提供了完整的操作步骤、配置命令及排错方法,帮助网络工程师掌握这一经典网络技术。

698

Atracsys 3D视觉定位导航系统 SDK 故障分级与 PoE+ 部署:手术机器人集成中的实时误差诊断、单线供电与现场重校准

位姿计算可以拆成四步:两台相机同步成像;提取反光球、反光片或红外 LED 的像点;按标定参数做三角测量,得到每个特征点的三维坐标;再由标记几何先验解算 6DoF(x、y、z、α、β、γ)。工程难点不在公式,而在全视场并行采集、特征点编号不串、遮挡时仍能稳定输出,以及标定参数在全工作体积内的一致性。

欣佰特cnbestec的博客 264

60、拓扑码:从理论到应用

本博客深入探讨了拓扑码的基本理论、构造方法及其在量子计算中的应用。重点介绍了颜色码的拓扑起源、编码量子比特的计算方法、弦算子的构造与性质、边界条件的影响、同调描述以及与统计模型相关的错误阈值分析。同时,还涵盖了拓扑码的历史发展、与其他拓扑码的关系、与凝聚态系统的联系,以及其在激发态和高维中的表现。博客展示了拓扑码在容错量子计算和未来量子信息领域的广阔前景。

o1p2q3r的博客 129

如何使用Agent框架来模拟群体智能?

Agent 框架作为人工智能领域的重要工具,可以模拟复杂的群体智能行为,广泛应用于社会科学、经济学和行为学等领域。

335

语音机器人方言识别效果差怎么优化?本地小店场景调优指南(2026)

内容基于门店语音接待配置和方言场景排障的实际过程整理。ASR 热词权重机制、置信度分级、方言模型覆盖范围等描述,参考主流云 ASR 和语音机器人平台的通用设计思路;文中 CER、意图准确率、澄清次数、兜底率等阈值为本地实测经验值,第七节案例样本量有限(50 通录音),数字仅作方向性参考,不构成对任何方言或地区的普适结论。具体参数以平台文档和本地实测结果为准,文中配置片段为示意,不构成具体产品配置指南。

weixin_47312655的博客 324

深入浅出:使用Python爬取链家二手房数据

在数据驱动的时代,房地产数据作为社会经济的重要指标,一直是数据分析师、投资者和研究人员关注的焦点。链家作为国内领先的房产中介平台,其二手房数据具有较高的真实性和时效性。然而,官方API的访问限制和数据获取的门槛,让很多初学者望而却步。本文将带你从零开始,使用Python编写一个符合robots.txt规范的链家二手房爬虫,获取标题、户型、价格等信息。文章不仅会提供完整代码,还会深入讲解反爬策略、请求头伪装、数据解析、异常处理等实战技术。目录前言:为什么要写这篇博客?一、爬虫前的准备:法律与道德。

2201_76125261的博客 91

手术机器人薄膜开关的5项医疗级验证:从消毒剂兼容到寿命测试的工程落地指南

A:电气安全 IEC 60601-1 / GB 9706.1,生物相容性 ISO 10993-1,EMC 按 IEC 60601-1-2,ESD 按 IEC 61000-4-2,供应商体系要求 ISO 13485。如果你正在做手术机器人或医疗设备的整机设计,欢迎在评论区交流面板选型、消毒流程定义和防护结构设计中的实际问题。手术室常用 75% 酒精、含氯消毒液、过氧化氢湿巾反复擦拭,面板的油墨、面涂和背胶都要经得住。:接触放电常见 ±8 kV、空气放电 ±15 kV,面板的接地层和面涂要配合整机设计。

BSD_HY的博客 386

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

强化学习第十三课:为什么 Q-Table 到机器人这里会失效?

因为产生数据时可以使用:但学习目标却使用:代表更贪心的目标策略。所以:\[ \boxed{\text{行为策略和目标策略可以不同}} \]因为:它按照当前行为策略实际选择出的 \(a_{t+1}\) 来更新。所以采数据和被评价的是同一套策略。Q-Table 的最大问题:\[ \boxed{\text{状态空间太大/连续时无法枚举}} \]解决:用神经网络:代替 Q-Table。

qq_56657939的博客 266

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

机器人如何把目标变成运动:自由度、正逆运动学与奇异性

本文以2R平面机械臂为例,系统阐述机器人运动学核心概念:正运动学(FK)通过关节角计算末端位姿,逆运动学(IK)则反求实现目标位姿的关节构型,揭示同一位置可能对应多解或无解。引入雅可比矩阵描述关节速度与末端速度的映射关系,分析奇异性与工作空间限制,并强调实际应用中关节限位、碰撞避让及数值求解初值的重要性,阐明任务描述与机构能力之间的匹配本质。

weixin_37540091的博客 268

情感交互机器人融资,四川具身科技的「爱湫」登场

当前人形机器人的主流战场是工业、仓储、巡检这些「干活」的场景,而「爱湫」把重心放在了「陪伴」上——通过面部投影和情绪识别,让机器人和人之间多一层情感层面的交互。情感交互机器人的市场,其实一直存在,只是过去被「能干活」的主流叙事盖住了。它主推的产品「爱湫」,是一款主打情感交互的人形机器人,搭载 3D 投影面部技术,情感交互机器人的逻辑,是面向养老陪伴、文旅服务、展厅导览这类「不要求重体力、但要求互动感」的场景。真正的考验不在发布会,而在它能不能在真实的养老和陪伴场景里,被用户用起来、留下来。

jushenAGI的博客 250

第四章:[案例]基于本地知识库实现简易多轮对话机器人

本项目构建教学版本地知识库问答机器人,通过读取knowledge.txt文件,按标点切分文本,基于关键词匹配实现简单检索,并结合多轮对话历史与大模型生成答案。代码演示了检索增强生成(RAG)的核心流程,适用于学习理解,区别于生产级向量检索系统。

xyphf的博客 708

用于医疗的磁驱动柔性微机器人研究进展!

磁驱动柔性微机器人已从“能动的小软虫”走向器官适配、诊疗一体、可释药可成像的原型系统,下一步竞争焦点是体内闭环控制、材料长期安全性与临床级系统集成。你想先深入哪条技术线:磁化编程制造、体内闭环控制,还是具体器官(脑/胃/心/血管)应用?

m0_65595995的博客 194

数字孪生如何支持具身智能?从Real2Sim、机器人训练到Sim2Real闭环

真正的技术升级,是把过去用于“看世界、管空间”的数字孪生能力,进一步转化为机器人能够使用的场景重建、空间语义、物理仿真、虚拟验证、训练评测和运营环境。验证成熟的仿真平台等基础软硬件还应进一步扩大应用。从AES等数字孪生技术基础进一步发展到AperOne的选型评测、仿真训练和运营闭环,可以看作这一技术路线的具体实践:数字孪生不再只是描述现实世界,而开始成为机器人理解、验证和进入现实世界的中间层。**核心关键词:**数字孪生、具身智能、机器人仿真、场景重建、Real2Sim、Sim2Real、机器人评测。

Luckytiger09的博客 323

既有电梯机器人梯控选型:现场可行性评估方法

真正需要判断的是项目可行性:现有电梯属于什么类型、实际控制哪些楼层、机器人及载具能否完成物理通行、井道和轿厢通信是否满足业务要求、设备是否具备合理安装和供电条件,以及机器人系统能否与。更完整的结论应该回答:具体电梯是否属于适用范围,实际控制多少楼层,机器人及载具是否能够完成物理通行,井道与轿厢通信条件怎样,设备是否具备合理安装和供电基础,机器人系统是否能够完成业务对接,以及还有哪些条件需要进一步测试。先确认电梯适用性和实际控制楼层,再核查网络、空间、施工供电及机器人接口条件,最后进入配置选择。

Robustel的博客 302

从VLM规划器到机器人智能体:面向依赖历史信息的操控任务自演化框架

26年10月来自中科大、上海AI实验室、上海交通大学、阿联酋MBZAI和浙大的论文“From VLM Planners To Robotic Agents: Self-evolving Harnesses for History-dependent Manipulation”。SERA 的主要价值,在于把机器人的任务记忆、执行判断和失败修复组织成一个可修改、可验证的系统;论文支持这种系统设计能改善既定任务上的表现,但尚不足以证明广泛的自主进化与泛化能力。

yorkhunter的博客 1120

技术白皮书:工件变形、图纸对不上?机器人离线编程iRobotCAM如何实现激光熔覆精准加工

本文针对机器人激光熔覆中工件偏差、编程复杂、调试低效等难题,提出基于iRobotCAM离线编程软件的解决方案。通过融合双目视觉定位实现工件精准纠偏,CAD智能编程快速生成复杂曲面轨迹,虚拟仿真提前规避干涉,多品牌机器人后置适配打通全流程,推动从“按图加工”迈向“看件加工”,显著提升柔性制造效率与智能化水平。

专注工业软件的突破,实现具身智能在机器人自动化的落地 156

《启元 PrimeSim 如何用生成视频评估机器人策略》

PrimeSim 论文展示过一组把瓶子装进纸箱的生成画面。机械臂靠近传送带,抓取瓶子,再把它送向箱子。从抓取到装箱,画面连成了一次完整操作。如果开发者想用这样的模型挑选机器人策略,问题就变了。一条顺利结束的视频,怎样变成某个策略的成功率?换一套控制程序,生成的环境能否保留两者的强弱差别?启元 PrimeBot Research 的 PrimeSim 正在尝试回答这个问题。它以视频生成模型为起点,接收机器人动作,预测后续的多视角画面。

鱼干的博客 361

单台机器人的智能演示如何转向大规模异构集群稳定、可盈利的工程化落地。

在技术层面,稳石将持续输出仓储物理域集群智能体的落地经验,参与行业交流、案例研讨、产业标准与技术白皮书的研讨工作,把大规模异构集群协同、工业现场安全管控、实体场景智能体评估指标等一线实践经验,分享给产业链同仁,补齐物理域具身智能产业化的认知短板。同时携手深圳市机器人协会的众多会员伙伴,打通产学研用协同链路,共同破解工业具身智能规模化落地的工程化难题,为深圳建设全球领先的机器人与具身智能产业高地贡献力量,让集群智能体在更多实体仓储场景落地生根,赋能千行百业的供应链智能化升级。是稳石一贯坚持的产业底色。

wsrobot的博客 168

机器人与机电一体化3D数字孪生机器-Day10

复制来源:从已有的平台中复制,生成最后一个平台。旋转与位置:对最后一个平台进行旋转,并在 X 轴上减少 Z 方向的偏移量,使其与其他平台形成合理的角度关系。效果评估:整体布局虽然不是最优方案,但比没有模型要好,能够满足当前可视化需求。6. 为平台赋予不同颜色材质材质区分:为每个平台赋予不同的颜色材质,便于在场景中清晰区分各个平台。完成状态:平台搭建完成,多个转换器(Conveyor)已正常工作,整体结构可用于后续开发。

qq_53797018的博客 226

智驱服务·灵动随行:FOC直流无刷驱动器在商用服务机器人中的应用方案

近年来,随着人工智能、传感器与移动计算成本的快速下降,商用服务机器人正从展会展台走向真实商业场景:餐厅与酒店的配送机器人、商场与展馆的导览机器人、写字楼与机场的清洁机器人、医院的物流传送机器人……可以预见,随着集成化、智能化与能效安全的持续进步,这一类驱动方案将在更广阔的商业空间中,成为服务机器人无声而坚定的“步伐”。但也要看到,服务机器人走向真实商用仍面临驱动层面的现实约束:室内静音要求倒逼PWM频率与电机选型、长时间运行的温升与续航矛盾、多机同场的通信实时性,都对驱动方案提出了比工业场景更细腻的要求。

OIDFOC的博客 221
上一篇: 2026 ROS 2 Lyrical C++ 入门(五):Action 实战——任务反馈、取消与超时处理
下一篇: 2026 具身智能技术实战(一):VLA 到底怎么控制机器人?——用 LeRobot 跑通 SmolVLA 推理
风合星语
博客等级 码龄2年 1粉丝 27原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值