ROS2回调组和避免ROS2死锁

ROS2回调组

官方文档参考

  1. ros2的回调组callback_groups的官方文档(humble)
    http://docs.ros.org/en/humble/How-To-Guides/Using-callback-groups.html
  2. 前置参考:执行者executors http://docs.ros.org/en/humble/Concepts/Intermediate/About-Executors.html

执行者分为三种。

  1. Single-Threaded Executor
  2. Multi-Threaded Executor
  3. Static Single-Threaded Executor

如果不指定,默认为Single-Threaded Executor

回调组分为两种。

  1. Mutually Exclusive Callback Group(互斥回调组)
  2. Reentrant Callback Group(可重入回调组)

互斥回调组,防止回调并行执行。

例如

  1. 一个节点创建了3个服务(create_service),分别为serviceA,serviceB,serviceC。如果有其他节点在同一时刻使用client调用了serviceA,serviceB,serviceC,按照调用的先后顺序执行,如先执行serviceA,再执行serviceB,最后执行serviceC。
  2. 一个节点创建了一个服务(create_service)和一个订阅者(create_subscription),如果其他节点调用client和publish,那么service的回调callback和话题topic的subscription订阅回调callback不能同时执行。一个典型的现象是,在service回调callback执行过程中,无法同时执行subscription订阅回调callback,如果service回调callback中阻塞了5秒,那么在此5秒内不会触发subscription订阅回调callback,即使在此topic上一直有新数据(其他节点调用pulish发送新数据)。

上述的service和subscription等只是举例,任何涉及回调的都遵循,包括client,service,publish,subscription、action、timer等。

回调组适用于:(摘自官方文档):

  1. subscription callbacks (receiving and handling data from a topic),
  2. timer callbacks
  3. service callbacks (for executing service requests in a server),
  4. different callbacks in action servers and clients,
  5. done-callbacks of Futures

可重入回调组与互斥回调组相反,意味着可以同时(并发)执行。

不同的回调组总是可以同时(并发)执行。
例如

my_callback_group2 = MutuallyExclusiveCallbackGroup()
my_callback_group3 = MutuallyExclusiveCallbackGroup()
self.A = self.create_service(
            Trigger, '/serviceA', 
            self.callback1,
            callback_group=my_callback_group2,
            )
self.B = self.create_subscription(  
            Twist,
            '/my_result',
            self.callback2,
            10, 
            callback_group=my_callback_group3,
            )
self.C = self.create_subscription(  
            Twist,
            '/my_result_',
            self.callback3,
            10, 
            callback_group=my_callback_group2,
            )

此时一个订阅当执行callback1的过程中,例如5秒内,callback2也可并行执行,但是callback3无法并行执行,因为callback3和callback1在同一互斥回调组中。

具体回调组的示例和用法,可参考ros2的回调组callback_groups的官方文档(humble)http://docs.ros.org/en/humble/How-To-Guides/Using-callback-groups.html

需要注意的是:

  1. ros2中几乎处处是回调
  2. 有些回调是隐藏的,用户无法明显看到。(容易进坑)

避免死锁

因为上述原因,如果使用默认的回调组,默认回调组会将所有回调设置为一个互斥回调组,详见http://docs.ros.org/en/humble/How-To-Guides/Using-callback-groups.html,容易造成死锁。

If the user does not specify any callback group when creating a subscription, timer, etc., this entity will be assigned to the node’s default callback group. The default callback group is a Mutually Exclusive Callback Group and it can be queried via NodeBaseInterface::get_default_callback_group() in rclcpp and via Node.default_callback_group in rclpy.

避免死锁的方法:

  1. 如果在回调中使用回调,请设置不同的回调组或设置可重入回调组。
  2. 使用异步调用。

ps:ros2种python的client可以设置为同步调用和异步调用,C++的client只支持异步调用。
详见http://docs.ros.org/en/humble/How-To-Guides/Sync-Vs-Async.html

官方在文档http://docs.ros.org/en/humble/How-To-Guides/Using-callback-groups.html中举例了一个死锁的示例。
该示例中有一个节点,该节点有一个定时器和client,在定时回调中使用同步调用client,导致死锁。

死锁的原因分析:

  1. 在回调中使用了回调。在此示例中,定时器的回调中调用了client,而client异步有回调(done-callbacks of Futures)。
  2. 定时器回调和client回调使用了默认的回调,为同一个回调组且为互斥回调。
  3. 当定时器回调执行中时,互斥回调组生效,而client的回调组与定时器的回调组为同一个回调组,导致client异步发送后,无法触发回调(done-callbacks of Futures),如果在定时器回调中等待,例如python的await等,会导致死锁。
  4. 死锁时,函数在定时器回调中,互斥回调组生效,client的异步回调无法执行,而在定时器回调中又在等待client的结果。(但是永远等不到,因为client回调永远不会被触发,因为client回调和定时器回调在同一互斥回调组!!!)

知道了原理,可以防止死锁。例如官方的设置不同回调组等。

更新

扩展参考
https://nu-msr.github.io/ros_notes/ros2/spin.html
https://github.com/m-elwin/async_experiments
https://github.com/m2-farzan/ros2-asyncio
https://docs.ros2.org/latest/api/
https://github.com/tlangmo/ros2_asyncio (用ros2的executor实现的asyncio功能)

遇到死锁问题,可以尝试

  1. 设置回调组,如可重入回调组或不同回调组
  2. 将执行器设置为多线程执行器。
posted @ 2024-12-12 15:22  雪夜羽  阅读(814)  评论(0)    收藏  举报