同步与异步服务客户端

级别: 中级

时间: 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 调用进行抢占。一般情况下,回调函数应该只执行轻量且快速的操作。

警告

当发生死锁时,你将不会收到任何关于服务被阻塞的指示。不会有警告或异常抛出,堆栈跟踪中也没有指示,调用也不会失败。

2 异步调用

rclpy 中,异步调用是完全安全且推荐的调用服务的方法。与同步调用不同,它们可以在任何地方进行,而不会阻塞其他ROS和非ROS进程的运行风险。

在向服务发送请求后,异步客户端会立即返回一个表示调用和响应是否完成的 future 值(而不是响应本身的值)。可以随时查询返回的 future 是否有响应。

由于发送请求不会阻塞任何操作,可以使用循环在同一线程中既运行 rclpy 又检查 future,例如:

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

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

future 也可以使用定时器或回调来检索,就像在 这个示例 中一样,可以使用专用线程或其他方法。作为调用者,您可以自行决定如何存储 future,检查其状态并检索响应。

总结

不推荐实现同步服务客户端。它们容易发生死锁,但当死锁发生时不会提供任何问题指示。如果必须使用同步调用,可以使用 1 同步调用 部分中的示例来安全地进行。您还应该了解在 1.1 同步死锁 部分中概述的导致死锁的条件。我们推荐使用异步服务客户端。