同步与异步服务客户端

级别: 中级

时间: 10 分钟

简介

本指南旨在警告用户与 Python 同步服务客户端 call() API 相关的风险。

同步调用服务时很容易错误地导致死锁,因此我们不建议使用 call()

我们为希望使用同步调用并了解陷阱的有经验的用户提供了一个如何正确使用 call() 的示例。

我们还重点介绍了伴随同步调用的可能死锁场景。

由于我们建议避免同步调用,本指南还将介绍推荐的替代方案异步调用(“call_async()”)的功能和用法。

C++ 服务调用 API 仅在异步中可用,因此本指南中的比较和示例适用于 Python 服务和客户端。 此处给出的异步定义通常适用于 C++,但有一些例外。

1 同步调用

同步客户端在向服务发送请求时将阻塞调用线程,直到收到响应为止;调用期间该线程上不会发生任何其他事情。 调用可能需要任意长的时间才能完成。 完成后,响应将直接返回到客户端。

以下是如何从客户端节点正确执行同步服务调用的示例,类似于 简单服务和客户端 教程中的异步节点。

import sys
from threading import Thread

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node

class MinimalClientSync(Node):

    def __init__(self):
        super().__init__('minimal_client_sync')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.req = AddTwoInts.Request()

    def send_request(self):
        self.req.a = int(sys.argv[1])
        self.req.b = int(sys.argv[2])
        return self.cli.call(self.req)
        # This only works because rclpy.spin() is called in a separate thread below.
        # Another configuration, like spinning later in main() or calling this method from a timer callback, would result in a deadlock.

def main():
    rclpy.init()

    minimal_client = MinimalClientSync()

    spin_thread = Thread(target=rclpy.spin, args=(minimal_client,))
    spin_thread.start()

    response = minimal_client.send_request()
    minimal_client.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (minimal_client.req.a, minimal_client.req.b, response.sum))

    minimal_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

请注意,在 main() 中,客户端在单独的线程中调用 rclpy.spinsend_requestrclpy.spin 都是阻塞的,因此它们需要位于单独的线程中。

1.1 同步死锁

同步 call() API 有几种导致死锁的方式。

如上例注释中所述,未能创建单独的线程来旋转 rclpy 是导致死锁的原因之一。

当客户端阻塞等待响应的线程,但响应只能在同一个线程上返回时,客户端将永远不会停止等待,也不会发生任何其他事情。

死锁的另一个原因是通过在订阅、计时器回调或服务回调中同步调用服务来阻塞 rclpy.spin

例如,如果同步客户端的 send_request 放在回调中:

def trigger_request(msg):
    response = minimal_client.send_request()  # This will cause deadlock
    minimal_client.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (minimal_client.req.a, minimal_client.req.b, response.sum))
subscription = minimal_client.create_subscription(String, 'trigger', trigger_request, 10)

rclpy.spin(minimal_client)

发生死锁的原因是“rclpy.spin”不会通过“send_request”调用抢占回调。 一般来说,回调应该只执行轻量且快速的操作。

Warning

发生死锁时,您不会收到任何服务被阻止的指示。 不会抛出任何警告或异常,堆栈跟踪中也没有任何指示,并且调用不会失败。

2 异步调用

rclpy 中的异步调用完全安全,是推荐的服务调用方法。

与同步调用不同,它们可以从任何地方进行,不会有阻塞其他 ROS 和非 ROS 进程的风险。

异步客户端在向服务发送请求后将立即返回 future,该值指示调用和响应是否完成(而不是响应本身的值)。

可以随时查询返回的 future 以获取响应。

由于发送请求不会阻塞任何内容,因此可以使用循环在同一个线程中旋转 rclpy 并检查 future,例如:

while rclpy.ok():
    rclpy.spin_once(node)
    if future.done():
        #Get response

Python 的 简单服务和客户端 教程说明了如何执行异步服务调用并使用循环检索 future

还可以使用计时器或回调来检索 future,如 ``此示例 <https://github.com/ros2/examples/blob/rolling/rclpy/services/minimal_client/examples_rclpy_minimal_client/client_async_callback.py>`_、专用线程或通过其他方法。

作为调用者,您可以决定如何存储 future、检查其状态并检索您的响应。

摘要

不建议实现同步服务客户端。 它们容易发生死锁,但发生死锁时不会提供任何问题迹象。 如果您必须使用同步调用,则“1 同步调用”一节中的示例是一种安全的方法。 您还应该了解“1.1 同步死锁”一节中概述的导致死锁的条件。 我们建议改用异步服务客户端。