Clients de service synchrones ou asynchrones

Niveau : Intermédiaire

Durée : 10 minutes

Introduction

Ce guide est destiné à avertir les utilisateurs des risques associés à l’API call() du client de service synchrone Python. Il est très facile de provoquer par erreur un interblocage lors de l’appel de services de manière synchrone, nous ne recommandons donc pas d’utiliser call().

Nous fournissons un exemple sur la façon d’utiliser correctement call() pour les utilisateurs expérimentés qui souhaitent utiliser des appels synchrones et sont conscients des pièges. Nous mettons également en évidence les scénarios possibles de blocage qui l’accompagnent.

Étant donné que nous recommandons d’éviter les appels de synchronisation, ce guide abordera également les fonctionnalités et l’utilisation de l’alternative recommandée, les appels asynchrones (call_async()).

L’API d’appel de service C++ n’est disponible qu’en mode asynchrone, de sorte que les comparaisons et les exemples de ce guide concernent les services et les clients Python. La définition d’async donnée ici s’applique généralement à C++, à quelques exceptions près.

1 Appels synchrones

Un client synchrone bloquera le thread appelant lors de l’envoi d’une requête à un service jusqu’à ce qu’une réponse ait été reçue ; rien d’autre ne peut se produire sur ce fil pendant l’appel. L’appel peut prendre un temps arbitraire pour se terminer. Une fois terminée, la réponse revient directement au client.

Voici un exemple d’exécution correcte d’un appel de service synchrone à partir d’un nœud client, similaire au nœud asynchrone dans Simple Service and Client.

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()

Notez dans main() que le client appelle rclpy.spin dans un thread séparé. send_request et rclpy.spin sont bloquants, ils doivent donc être sur des threads séparés.

1.1 Blocage de synchronisation

L’API synchrone call() peut provoquer un blocage de plusieurs manières.

Comme mentionné dans les commentaires de l’exemple ci-dessus, ne pas créer un thread séparé pour lancer rclpy est une cause de blocage. Lorsqu’un client bloque un thread en attente d’une réponse, mais que la réponse ne peut être renvoyée que sur ce même thread, le client n’arrêtera jamais d’attendre et rien d’autre ne peut se produire.

Une autre cause de blocage est le blocage de rclpy.spin en appelant un service de manière synchrone dans un abonnement, un rappel de minuterie ou un rappel de service. Par exemple, si la send_request du client synchrone est placée dans un rappel :

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)

L’interblocage se produit car rclpy.spin ne préemptera pas le rappel avec l’appel send_request. En général, les rappels ne doivent effectuer que des opérations légères et rapides.

Avertissement

En cas d’interblocage, vous ne recevrez aucune indication indiquant que le service est bloqué. Il n’y aura pas d’avertissement ou d’exception levée, aucune indication dans la trace de la pile et l’appel n’échouera pas.

2 appels asynchrones

Les appels asynchrones dans rclpy sont entièrement sûrs et constituent la méthode recommandée pour appeler les services. Ils peuvent être effectués depuis n’importe où sans courir le risque de bloquer d’autres processus ROS et non ROS, contrairement aux appels de synchronisation.

Un client asynchrone retournera immédiatement future, une valeur qui indique si l’appel et la réponse sont terminés (pas la valeur de la réponse elle-même), après avoir envoyé une requête à un service. Le futur retourné peut être interrogé pour une réponse à tout moment.

Étant donné que l’envoi d’une requête ne bloque rien, une boucle peut être utilisée à la fois pour faire tourner rclpy et vérifier future dans le même thread, par exemple :

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

Le didacticiel Simple Service and Client pour Python illustre comment effectuer un appel de service asynchrone et récupérer le futur en utilisant une boucle.

Le futur peut également être récupéré à l’aide d’un minuteur ou d’un rappel, comme dans cet exemple <https://github.com/ros2/examples/blob/rolling/rclpy/services/minimal_client/examples_rclpy_minimal_client/client_async_callback.py> `_, un thread dédié, ou par une autre méthode. C'est à vous, en tant qu'appelant, de décider comment stocker ``future`, vérifier son statut et récupérer votre réponse.

Résumé

Il n’est pas recommandé d’implémenter un client de service synchrone. Ils sont susceptibles de se bloquer, mais ne fourniront aucune indication de problème en cas de blocage. Si vous devez utiliser des appels synchrones, l’exemple de la section 1 Appels synchrones est une méthode sûre pour le faire. Vous devez également être conscient des conditions qui provoquent un blocage décrites dans la section 1.1 Blocage de synchronisation. Nous vous recommandons d’utiliser plutôt des clients de service asynchrones.