Using coroutines with service calls

Coroutines allow suspending functions and resuming them later. In context of ROS2 programs, this is very useful when calling services. If you do not use coroutines, the options used are usually either blocking on the future (cannot be done in single threaded context) or manually checking it (requires timers for checking readiness) or specifying callbacks. Coroutines solve this by writing code, that looks almost like the blocking case, while being functionally similar to writing the rest of the function as a callback.

Note

This tutorial covers how to use coroutines specifically with service client in mrs_lib. If you want more general information and how they work, see eg. https://cppreference.com/cpp/language/coroutines.

Important parts

In this section, we will walk through the important parts of the example. The full source code can be found at the bottom of this page or in the examples folder of mrs_lib repo.

Coroutines

Syntactically, coroutines have two major differences normal C++ functions.

  1. Firstly, they must contain at least one of the co_* keywords (co_await, co_return or co_yield).

  2. Secondly, the return type must be a coroutine return type. For our usecase, it is enough to know that mrs_lib::Task<T> is such a type. The template parameter T is the type returned from the coroutine when awaited (defaults to void).

An example coroutine is shown in the following listing. The coroutine returns std::optional<std::shared_ptr<ServiceType::Response>> which is wrapped in mrs_lib::Task. It takes parameters just like a normal function.

Instead of the normal return keyword, we must use co_return (just coroutine version of the same). The co_await keyword in front of the client call tells the language to suspend the current coroutine here and resume once the awaited result is ready. In this case, it means it sends the service request, suspends the coroutine, and resumes it once the server’s response arives.

Example coroutine.
36mrs_lib::Task<std::optional<std::shared_ptr<ServiceType::Response>>> call_service(bool data)
37{
38  auto request = std::make_shared<ServiceType::Request>();
39  request->data = data;
40  co_return co_await service_client_.callAwaitable(request);
41}

As you can see, the coroutine function looks almost like a normal function, just with some co_* keywords sprinkled in.

Next we will have a look at a second coroutine in our program. This time, it will be a timer callback.

Example coroutine callback.
45mrs_lib::Task<> service_timer_callback()
46{
47  RCLCPP_INFO_STREAM(logger, "Calling service...");
48  std::optional<std::shared_ptr<ServiceType::Response>> response_opt = co_await call_service(true);
49  if (response_opt.has_value())
50  {
51    auto response = response_opt.value();
52    std::string response_str = std::format("success: '{}'\n  message: '{}'", response->success, response->message);
53    RCLCPP_INFO_STREAM(logger, "Service response:\n" << response_str);
54  } else
55  {
56    RCLCPP_WARN_STREAM(logger, "Failed to call service!!!");
57  }
58}

Once again, you can see it uses the co_await keyword. This is required whenever you are calling another coroutine, even if it returns void.

You may also notice that there is no co_return this time. Similarly to normal functions, if the coroutine returns void, you can omit the co_return at the end. However, you still have to have at least one co_* keyword in it to make it a coroutine.

Warning

The coroutines implemented in mrs_lib are so called lazy coroutines. This means they do nothing until you await them (eg. co_await my_coro()). As a result, if you forget to await a coroutine, it will not run!

Coroutines as callbacks

You can use coroutines as callbacks, but there are currently some additional restrictions:

  1. The object which calling the callbacks must support calling coroutines. (See API reference for the individual callback sources if they have overloads for coroutine callbacks.)

  2. The rclcpp::CallbackGroup used must be reentrant [1].

In the following listing, you can see the constructor of the example node. Firstly, we initialize the parent node, a local copy of the logger, the required reentrant callback group and the service client. Next we initialize the timer with our coroutine callback. For timer, we use a constructor overload taking mrs_lib::TimerHandlerOptions (contains node and callback group), rate and the callback. The coroutine callback is passed as two parts – the method pointer and a pointer to the instance with which it is invoked. Finally, we also initialize another timer to see the executor is not blocked during the service call.

Constructor of the example node.
18CoroServiceExample(const rclcpp::NodeOptions& opts)
19    : Node("coro_service_example", opts), logger(this_node().get_logger()),
20      reentrant_callback_group_(this_node().create_callback_group(rclcpp::CallbackGroupType::Reentrant)),
21      service_client_(this_node_ptr(), "my_service", nullptr),
22      service_timer_(std::make_unique<mrs_lib::ROSTimer>(std::invoke([this] {
23                                                           mrs_lib::TimerHandlerOptions opts{this_node_ptr()};
24                                                           opts.callback_group = reentrant_callback_group_;
25                                                           return opts;
26                                                         }),
27                                                         rclcpp::Rate(5s), &CoroServiceExample::service_timer_callback, this)),
28      chatter_timer_(
29          std::make_unique<mrs_lib::ROSTimer>(this_node_ptr(), rclcpp::Rate(1s), std::bind_front(&CoroServiceExample::chatter_timer_callback, this)))
30{
31}

How to run

The example is automatically built when building mrs_lib with tests enabled. Alternatively, you can copy both the example and CMakeLists.txt file from the repo to your package.

To run the example, you have to start both the server and the client. (Don’t forget to source your workspace.)

Running server.
$ ros2 run mrs_lib example_coro_service_server
[INFO] [...]: Created service: /my_service
[INFO] [...]: Received service call. Starting work...
[INFO] [...]: Work done. Sending response...
[INFO] [...]: Received service call. Starting work...
[INFO] [...]: Work done. Sending response...
Running client.
$ ros2 run mrs_lib example_coro_service
[INFO] [...]: Created client 'my_service' -> '/my_service'
[INFO] [...]: Chattering ... (0)
[INFO] [...]: Chattering ... (1)
[INFO] [...]: Chattering ... (2)
[INFO] [...]: Chattering ... (3)
[INFO] [...]: Calling service...
[INFO] [...]: Chattering ... (4)
[INFO] [...]: Chattering ... (5)
[INFO] [...]: Service response:
success: 'true'
message: 'Response to service with value: 'true''
[INFO] [...]: Chattering ... (6)
[INFO] [...]: Chattering ... (7)
[INFO] [...]: Chattering ... (8)
[INFO] [...]: Calling service...
[INFO] [...]: Chattering ... (9)
[INFO] [...]: Chattering ... (10)
[INFO] [...]: Service response:
success: 'true'
message: 'Response to service with value: 'true''
[INFO] [...]: Chattering ... (11)

From the output of the client, we can see that the chattering timer is running even during the service call. During the processing of the service call, the coroutine is suspended and thus it can handle the other callbacks, such as the chatter. When the service server finishes around a second later, the coroutine is resumed and it prints the response.

Full Source code

This is the full source code for the example. You can also find it in the examples directory of mrs_lib repo.

Full code for the example.
  1#include <std_srvs/srv/set_bool.hpp>
  2#include <mrs_lib/coro/task.hpp>
  3#include <mrs_lib/node.h>
  4#include <mrs_lib/service_client_handler.h>
  5#include <mrs_lib/service_server_handler.h>
  6#include <mrs_lib/timer_handler.h>
  7
  8
  9namespace mrs_lib_examples
 10{
 11  using namespace std::chrono_literals;
 12  using ServiceType = std_srvs::srv::SetBool;
 13
 14  class CoroServiceExample : public mrs_lib::Node
 15  {
 16  public:
 17    // BEGIN CTOR
 18    CoroServiceExample(const rclcpp::NodeOptions& opts)
 19        : Node("coro_service_example", opts), logger(this_node().get_logger()),
 20          reentrant_callback_group_(this_node().create_callback_group(rclcpp::CallbackGroupType::Reentrant)),
 21          service_client_(this_node_ptr(), "my_service", nullptr),
 22          service_timer_(std::make_unique<mrs_lib::ROSTimer>(std::invoke([this] {
 23                                                               mrs_lib::TimerHandlerOptions opts{this_node_ptr()};
 24                                                               opts.callback_group = reentrant_callback_group_;
 25                                                               return opts;
 26                                                             }),
 27                                                             rclcpp::Rate(5s), &CoroServiceExample::service_timer_callback, this)),
 28          chatter_timer_(
 29              std::make_unique<mrs_lib::ROSTimer>(this_node_ptr(), rclcpp::Rate(1s), std::bind_front(&CoroServiceExample::chatter_timer_callback, this)))
 30    {
 31    }
 32    // END CTOR
 33
 34  private:
 35    // BEGIN CORO
 36    mrs_lib::Task<std::optional<std::shared_ptr<ServiceType::Response>>> call_service(bool data)
 37    {
 38      auto request = std::make_shared<ServiceType::Request>();
 39      request->data = data;
 40      co_return co_await service_client_.callAwaitable(request);
 41    }
 42    // END CORO
 43
 44    // BEGIN CORO CALLBACK
 45    mrs_lib::Task<> service_timer_callback()
 46    {
 47      RCLCPP_INFO_STREAM(logger, "Calling service...");
 48      std::optional<std::shared_ptr<ServiceType::Response>> response_opt = co_await call_service(true);
 49      if (response_opt.has_value())
 50      {
 51        auto response = response_opt.value();
 52        std::string response_str = std::format("success: '{}'\n  message: '{}'", response->success, response->message);
 53        RCLCPP_INFO_STREAM(logger, "Service response:\n" << response_str);
 54      } else
 55      {
 56        RCLCPP_WARN_STREAM(logger, "Failed to call service!!!");
 57      }
 58    }
 59    // END CORO CALLBACK
 60
 61
 62    void chatter_timer_callback()
 63    {
 64      RCLCPP_INFO_STREAM(logger, "Chattering ... (" << message_number_ << ")");
 65      message_number_ += 1;
 66    }
 67
 68    rclcpp::Logger logger;
 69
 70    size_t message_number_ = 0;
 71
 72    std::shared_ptr<rclcpp::CallbackGroup> reentrant_callback_group_;
 73    mrs_lib::ServiceClientHandler<ServiceType> service_client_;
 74    std::unique_ptr<mrs_lib::MRSTimer> service_timer_;
 75    std::unique_ptr<mrs_lib::MRSTimer> chatter_timer_;
 76  };
 77
 78
 79  class CoroServiceExampleServer : public mrs_lib::Node
 80  {
 81  public:
 82    CoroServiceExampleServer(const rclcpp::NodeOptions& opts)
 83        : Node("coro_service_example_server", opts), logger(this_node().get_logger()),
 84          service_server_(this_node_ptr(), "my_service", std::bind_front(&CoroServiceExampleServer::service_callback, this))
 85    {
 86    }
 87
 88  private:
 89    void service_callback(std::shared_ptr<ServiceType::Request> req, std::shared_ptr<ServiceType::Response> res)
 90    {
 91      RCLCPP_INFO_STREAM(logger, "Received service call. Starting work...");
 92
 93      // Simulated work...
 94      // Warning: This blocks the executor.
 95      // If this runs on a single threaded executor, no other callbacks
 96      // on ANY node in this executor will be called until this one finishes.
 97      // In the case of this example, it is ok, but you should be aware of this problem.
 98      std::this_thread::sleep_for(1s);
 99      res->success = true;
100      res->message = std::format("Response to service with value: '{}'", req->data);
101
102      RCLCPP_INFO_STREAM(logger, "Work done. Sending response...");
103    }
104
105    rclcpp::Logger logger;
106
107    mrs_lib::ServiceServerHandler<ServiceType> service_server_;
108  };
109
110} // namespace mrs_lib_examples
111
112#include <rclcpp_components/register_node_macro.hpp>
113RCLCPP_COMPONENTS_REGISTER_NODE(mrs_lib_examples::CoroServiceExample)
114RCLCPP_COMPONENTS_REGISTER_NODE(mrs_lib_examples::CoroServiceExampleServer)

Footnotes