ROS2のService通信で、同期的に処理したい (Python)
ROS2のService通信で、レスポンスを受け取ってから次の処理に進む、という同期的な書き方をしたかった。
call() は使いたくない
rclpyには同期版の call() メソッドがあり、これを使えば結果が返るまでブロックできる。ただし、コールバックの中から呼ぶとDead Lockを招きやすい。スピンが止まっている状態でレスポンスを待つことになり、レスポンスが永久に処理されないからである。
(そのため、そもそもrclcpp(C++)には同期版のAPIが存在せず、非同期版しかサポートされていない)
では call_async() を使うとして、これをスマートに同期的な処理として書けないか。調べてみると main 関数の中で spin_until_future_complete() をゴニョゴニョする例ばかりで、呼び出し箇所とノードの実装が分離してしまい、複雑なコードでは扱いづらい。
結論: async/await とコールバックグループを使う
- コールバックを
async defで定義し、call_async()が返すFutureをawaitする - サービスクライアントを、
awaitする側のコールバックとは別のMutuallyExclusiveCallbackGroupに入れる
await した時点でそのコールバックの実行が中断され、executorのスピンループに制御が戻る。だからスピンが止まらず、レスポンスが処理されてFutureが完了し、コールバックの続きが再開される。クライアントを別のコールバックグループに置くのは、await 中のコールバックと同じグループだとレスポンスの処理が始められないためである。
MultiThreadedExecutor は必要なく、rclpy.spin() のままでよい。
実装例
import rclpy
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
from rclpy.node import Node
from std_srvs.srv import Empty
class AwaitClient(Node):
def __init__(self):
super().__init__('await_client')
self.cbgroup = MutuallyExclusiveCallbackGroup()
# サービスクライアントは専用のコールバックグループに入れる
self._client = self.create_client(Empty, 'delay', callback_group=self.cbgroup)
# await する側のコールバックはデフォルトのコールバックグループのまま
self._tmr = self.create_timer(5.0, self.timer_callback)
async def timer_callback(self):
self.get_logger().info('calling delay')
# await した時点で実行が中断され、スピンループに制御が戻る
await self._client.call_async(Empty.Request())
self.get_logger().info('done!')
def main(args=None):
rclpy.init(args=args)
rclpy.spin(AwaitClient())
rclpy.shutdown()
main 側には何の仕掛けも要らず、待ちたい箇所にそのまま await と書けるのでかなりスマートに書ける。
以下のリポジトリに、この書き方とDead Lockする書き方が並べて置かれていて分かりやすかった。
async_experiments/async_experiments/async_client.py at 68c9edbb174d5e1c53a19b883b4d0329e2360124 · m-elwin/async_experimentsAsynchronous service client experiments for ros2. Contribute to m-elwin/async_experiments development by creating an account on GitHub.参考
Synchronous vs. asynchronous service clients — ROS 2 Documentation
Using callback groups — ROS 2 Documentation: Humble documentation