ROS2回调组和避免ROS2死锁
ROS2回调组
官方文档参考
- ros2的回调组callback_groups的官方文档(humble)
http://docs.ros.org/en/humble/How-To-Guides/Using-callback-groups.html - 前置参考:执行者executors http://docs.ros.org/en/humble/Concepts/Intermediate/About-Executors.html
执行者分为三种。
- Single-Threaded Executor
- Multi-Threaded Executor
- Static Single-Threaded Executor
如果不指定,默认为Single-Threaded Executor
回调组分为两种。
- Mutually Exclusive Callback Group(互斥回调组)
- Reentrant Callback Group(可重入回调组)
互斥回调组,防止回调并行执行。
例如
- 一个节点创建了3个服务(create_service),分别为serviceA,serviceB,serviceC。如果有其他节点在同一时刻使用client调用了serviceA,serviceB,serviceC,按照调用的先后顺序执行,如先执行serviceA,再执行serviceB,最后执行serviceC。
- 一个节点创建了一个服务(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等。
回调组适用于:(摘自官方文档):
- subscription callbacks (receiving and handling data from a topic),
- timer callbacks
- service callbacks (for executing service requests in a server),
- different callbacks in action servers and clients,
- 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
需要注意的是:
- ros2中几乎处处是回调
- 有些回调是隐藏的,用户无法明显看到。(容易进坑)
避免死锁
因为上述原因,如果使用默认的回调组,默认回调组会将所有回调设置为一个互斥回调组,详见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.
避免死锁的方法:
- 如果在回调中使用回调,请设置不同的回调组或设置可重入回调组。
- 使用异步调用。
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,导致死锁。
死锁的原因分析:
- 在回调中使用了回调。在此示例中,定时器的回调中调用了client,而client异步有回调(done-callbacks of Futures)。
- 定时器回调和client回调使用了默认的回调,为同一个回调组且为互斥回调。
- 当定时器回调执行中时,互斥回调组生效,而client的回调组与定时器的回调组为同一个回调组,导致client异步发送后,无法触发回调(done-callbacks of Futures),如果在定时器回调中等待,例如python的await等,会导致死锁。
- 死锁时,函数在定时器回调中,互斥回调组生效,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功能)
遇到死锁问题,可以尝试
- 设置回调组,如可重入回调组或不同回调组
- 将执行器设置为多线程执行器。

浙公网安备 33010602011771号