同步与异步服务客户端
级别: 中级
时间: 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.spin
。send_request
和 rclpy.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
,检查其状态并检索响应。