фон
Приложения ROS обычно состоят из отдельных «узлов», которые выполняют одну задачу и отделены от остальной системы. Это способствует изоляции ошибок, ускорению разработки, модульности и повторному использованию кода, но часто за счет производительности. После первоначальной разработки ROS1 необходимость в эффективной комбинации узлов стала очевидной, поэтому были разработаны Nodelets. В ROS2 целью является улучшение конструкции узлов путем решения некоторых основных проблем, требующих реконструкции узлов.
Основное содержание статьи
В этой демонстрации мы сосредоточимся на том, как вручную объединять узлы.,Метод заключается в том, чтобы определить узлы отдельно.,но комбинируйте их в разных схемах процессов,без изменения кода узла и ограничения его функциональности。Отображается при использовании Когда std::unique_ptr публикует и подписывается, он реализует внутрипроцессные соединения публикации/подписки и может обеспечить передачу сообщений без копирования.
Для начала давайте взглянем на исходный код:
#include <chrono>
#include <cinttypes>
#include <cstdio>
#include <memory>
#include <string>
#include <utility>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
using namespace std::chrono_literals;
// Node that produces messages.
struct Producer : public rclcpp::Node
{
Producer(const std::string & name, const std::string & output)
: Node(name, rclcpp::NodeOptions().use_intra_process_comms(true))
{
// Create a publisher on the output topic.
pub_ = this->create_publisher<std_msgs::msg::Int32>(output, 10);
std::weak_ptr<std::remove_pointer<decltype(pub_.get())>::type> captured_pub = pub_;
// Create a timer which publishes on the output topic at ~1Hz.
auto callback = [captured_pub]() -> void {
auto pub_ptr = captured_pub.lock();
if (!pub_ptr) {
return;
}
static int32_t count = 0;
std_msgs::msg::Int32::UniquePtr msg(new std_msgs::msg::Int32());
msg->data = count++;
printf(
"Published message with value: %d, and address: 0x%" PRIXPTR "\n", msg->data,
reinterpret_cast<std::uintptr_t>(msg.get()));
pub_ptr->publish(std::move(msg));
};
timer_ = this->create_wall_timer(1s, callback);
}
rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
// Node that consumes messages.
struct Consumer : public rclcpp::Node
{
Consumer(const std::string & name, const std::string & input)
: Node(name, rclcpp::NodeOptions().use_intra_process_comms(true))
{
// Create a subscription on the input topic which prints on receipt of new messages.
sub_ = this->create_subscription<std_msgs::msg::Int32>(
input,
10,
[](std_msgs::msg::Int32::UniquePtr msg) {
printf(
" Received message with value: %d, and address: 0x%" PRIXPTR "\n", msg->data,
reinterpret_cast<std::uintptr_t>(msg.get()));
});
}
rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr sub_;
};
int main(int argc, char * argv[])
{
setvbuf(stdout, NULL, _IONBF, BUFSIZ);
rclcpp::init(argc, argv);
rclcpp::executors::SingleThreadedExecutor executor;
auto producer = std::make_shared<Producer>("producer", "number");
auto consumer = std::make_shared<Consumer>("consumer", "number");
executor.add_node(producer);
executor.add_node(consumer);
executor.spin();
rclcpp::shutdown();
return 0;
}
Вы можете видеть, что Producer наследует от Node и устанавливает для use_intra_process_comms значение true, чтобы можно было использовать механизм внутри_процесса. Вы можете видеть, что Producer наследует от Node и устанавливает для use_intra_process_comms значение true, чтобы можно было использовать механизм внутри_процесса. Как вы можете видеть, взглянув на функцию main, у нас есть узел-производитель и потребитель, добавьте их в однопоточный исполнитель, а затем вызовите spin. Глядя на реализацию узла «производитель» в структуре производителя, вы можете видеть, что мы создали публикатора, который публикует «числовую» тему, и таймер, который регулярно создает новые сообщения, распечатывает адрес в памяти и значение его содержимого. и публикует его. Узел «потребитель» немного проще, и его реализацию можно увидеть в структуре потребителя, поскольку он подписывается только на тему «номер» и печатает адрес и значение получаемых сообщений.
Производитель распечатывает адрес и соответствующее значение, как и ожидалось, а потребитель распечатывает соответствующий адрес и соответствующее значение. Это показывает, что внутрипроцессное взаимодействие действительно работает и позволяет избежать ненужного копирования, по крайней мере, для простых изображений.
При выполнении исполняемого файла ros2 runtra_process_demo two_node_pipeline результаты печати будут следующими:
Сообщения отправляются со скоростью примерно одно в секунду. Это потому, что мы сказали таймеру срабатывать примерно раз в секунду. Кроме того, мы замечаем, что первое сообщение (со значением 0) не имеет соответствующей строки «Полученное сообщение...». Это связано с тем, что публикация/подписка является «максимальной мерой» и не допускает какого-либо поведения, подобного «блокировке». Это означает, что если издатель опубликует сообщение до того, как будет установлена подписка, подписка не получит это сообщение. Это состояние гонки может привести к потере первых нескольких сообщений. В этом случае, поскольку они происходят только один раз в секунду, обычно теряется только первое сообщение. Наконец, вы можете видеть, что строки «Опубликованное сообщение...» и «Полученное сообщение...» с одинаковым значением также имеют один и тот же адрес. Это указывает на то, что полученный адрес сообщения совпадает с опубликованным адресом сообщения и не является копией.
Это потому, что мы публикуем и подписываемся на std::unique_ptrs.,Это позволяет владельцам сообщений безопасно перемещаться внутри системы.。Конечно, вы также можете использоватьconst&иstd::shared_ptr发布и订阅,Но нулевой копии в этом случае нет.
Вот объяснение использования и разницы между std::unique_ptr и std::shared_ptr.
Роль интеллектуальных указателей такова: Интеллектуальные указатели используются для обеспечения возможности автоматического удаления объектов, когда они больше не используются.
std::unique_ptr :одинstd::unique_ptr会指向один对象且不允许其他указатель指向。创建одинuniqueуказатель,Поместить в контейнер (например, карту),Но возвращает фактический указатель,Назначьте другому объекту.полю. Объект, на который указывает std::unique_ptr, можно переместить в другой указатель.,Когда заостренный предмет умирает,Контейнер автоматически освободит все ресурсы. поэтому,std::unique_ptr играет роль в управлении ресурсами памяти.,Настоящие указатели можно использовать везде.
std::shared_ptr:Сам жизненный цикл относительно фиксирован,std::shared_ptr разделяет указатель между несколькими объектами,Жизненный цикл этих объектов относительно динамичен.,Указатели освобождаются, когда все объекты завершены.
Таким образом, std::shared_ptr — это указатель на разделяемую память, а std::unique_ptr — это указатель, которому не нужно управлять памятью самостоятельно.
Цикл нулевой копии
Эта демонстрация похожа на предыдущую, но вместо того, чтобы производитель создавал новое сообщение для каждой итерации, в этой демонстрации используется только один экземпляр сообщения. Это достигается путем создания цикла и «запуска» связи через внешний узел перед тем, как исполнитель функции обратного вызова опубликует сообщение:
#include <chrono>
#include <cinttypes>
#include <cstdio>
#include <memory>
#include <string>
#include <utility>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"
using namespace std::chrono_literals;
// This node receives an Int32, waits 1 second, then increments and sends it.
struct IncrementerPipe : public rclcpp::Node
{
IncrementerPipe(const std::string & name, const std::string & in, const std::string & out)
: Node(name, rclcpp::NodeOptions().use_intra_process_comms(true))
{
// Create a publisher on the output topic.
pub = this->create_publisher<std_msgs::msg::Int32>(out, 10);
std::weak_ptr<std::remove_pointer<decltype(pub.get())>::type> captured_pub = pub;
// Create a subscription on the input topic.
sub = this->create_subscription<std_msgs::msg::Int32>(
in,
10,
[captured_pub](std_msgs::msg::Int32::UniquePtr msg) {
auto pub_ptr = captured_pub.lock();
if (!pub_ptr) {
return;
}
printf(
"Received message with value: %d, and address: 0x%" PRIXPTR "\n", msg->data,
reinterpret_cast<std::uintptr_t>(msg.get()));
printf(" sleeping for 1 second...\n");
if (!rclcpp::sleep_for(1s)) {
return; // Return if the sleep failed (e.g. on ctrl-c).
}
printf(" done.\n");
msg->data++; // Increment the message's data.
printf(
"Incrementing and sending with value: %d, and address: 0x%" PRIXPTR "\n", msg->data,
reinterpret_cast<std::uintptr_t>(msg.get()));
pub_ptr->publish(std::move(msg)); // Send the message along to the output topic.
});
}
rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr pub;
rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr sub;
};
int main(int argc, char * argv[])
{
setvbuf(stdout, NULL, _IONBF, BUFSIZ);
rclcpp::init(argc, argv);
rclcpp::executors::SingleThreadedExecutor executor;
// Create a simple loop by connecting the in and out topics of two IncrementerPipe's.
// The expectation is that the address of the message being passed between them never changes.
auto pipe1 = std::make_shared<IncrementerPipe>("pipe1", "topic1", "topic2");
auto pipe2 = std::make_shared<IncrementerPipe>("pipe2", "topic2", "topic1");
rclcpp::sleep_for(1s); // Wait for subscriptions to be established to avoid race conditions.
// Publish the first message (kicking off the cycle).
std::unique_ptr<std_msgs::msg::Int32> msg(new std_msgs::msg::Int32());
msg->data = 42;
printf(
"Published first message with value: %d, and address: 0x%" PRIXPTR "\n", msg->data,
reinterpret_cast<std::uintptr_t>(msg.get()));
pipe1->pub->publish(std::move(msg));
executor.add_node(pipe1);
executor.add_node(pipe2);
executor.spin();
rclcpp::shutdown();
return 0;
}
Отличие от предыдущего примера в том, что в этой демонстрации используется только один узел.,用不同的名称и配置实例化了两次。эта линияpipe1->pub->pub(msg);начать процесс,но с тех пор,Каждый узел вызывает публикацию в своей собственной функции обратного вызова подписки.,Сообщения передаются туда и обратно между узлами. Ожидается, что узел будет доставлять сообщения туда и обратно один раз в секунду.,Каждый раз увеличивает значение сообщения. Потому что сообщение опубликовано и подписано как unique_ptr.,Таким образом, то же самое сообщение, созданное вначале, будет использоваться и дальше. Результаты, распечатанные после запуска, следующие:
Published first message with value: 42, and address: 0x7fd2ce0a2bc0
Received message with value: 42, and address: 0x7fd2ce0a2bc0
sleeping for 1 second...
done.
Incrementing and sending with value: 43, and address: 0x7fd2ce0a2bc0
Received message with value: 43, and address: 0x7fd2ce0a2bc0
sleeping for 1 second...
done.
Incrementing and sending with value: 44, and address: 0x7fd2ce0a2bc0
Received message with value: 44, and address: 0x7fd2ce0a2bc0
sleeping for 1 second...
done.
Incrementing and sending with value: 45, and address: 0x7fd2ce0a2bc0
Received message with value: 45, and address: 0x7fd2ce0a2bc0
sleeping for 1 second...
done.
Incrementing and sending with value: 46, and address: 0x7fd2ce0a2bc0
Received message with value: 46, and address: 0x7fd2ce0a2bc0
sleeping for 1 second...
done.
Incrementing and sending with value: 47, and address: 0x7fd2ce0a2bc0
Received message with value: 47, and address: 0x7fd2ce0a2bc0
sleeping for 1 second...
[...]
Отсюда вы можете видеть, что число продолжает увеличиваться на каждой итерации, начиная с 42... и на протяжении всего процесса оно повторно использует одно и то же сообщение, а адрес его указателя никогда не меняется, что позволяет избежать ненужных копий.
Поэтому, когда нам нужно передать большое количество изображений или данных облака точек в нашем проекте, мы можем использовать этот метод для достижения эффективной связи между процессами. Далее мы реализуем демонстрацию передачи изображений opencv с использованием OpenCV Capture images и аннотирования изображений. и просматривать изображения.
Сначала будет конвейер, состоящий из трёх узлов,Как показано ниже:camera_node->watermark_node->image_view_node
camera_node считывает необработанное изображение с номера устройства камеры на компьютере, записывает некоторую информацию об изображении и публикует ее. Watermark_node подписывается на выходные данные camera_node и добавляет к изображению некоторую текстовую информацию перед публикацией. Наконец, image_view_node подписывается на выходные данные Watermark_node, записывает дополнительную текстовую информацию на изображение, а затем использует cv::imshow для его визуализации.
В каждом узле адрес отправляемого или полученного сообщения записывается в изображение. Узлы информации о водяных знаках и визуализации изображения предназначены для изменения изображения без копирования изображения. Поэтому, пока узлы находятся в одном и том же месте. процесс, а изображение остается в процессе, как описано выше, и адрес, напечатанный на изображении, должен быть тот же. Ходовой узел такой, как показано на рисунке
Здесь вы можете приостановить рендеринг изображения, нажав клавишу пробела, а затем снова нажав клавишу пробела, чтобы продолжить рендеринг. Вы также можете нажать q или ESC для выхода. Если вы приостановите просмотр изображений, вы сможете сравнить адреса, написанные на изображении, и проверить, совпадают ли они.
Визуализация процесса с помощью двух изображений
В этом примере есть два узла визуализации изображений, все еще находящиеся в одном процессе, но теперь должны отображаться два окна визуализации изображений.
Как и в предыдущем примере, рендеринг можно приостановить с помощью клавиши пробела, а затем продолжить, снова нажав клавишу пробела. При этом проверка разрешения на обновление прекращается, чтобы увидеть, изменился ли указатель на экране. Как вы можете видеть на примере изображения выше, у нас есть одно изображение со всеми одинаковыми указателями, а затем другое изображение с теми же указателями, что и первое изображение, но последний указатель на втором изображении отличается. Чтобы понять, почему это происходит, подумайте о топологии графа:
camera_node -> watermark_node -> image_view_node-> image_view_node2
Ссылка между camera_node и Watermark_node может использовать один и тот же указатель без копирования, поскольку только одна подписка в процессе должна доставлять ему сообщения. Но для связи между Watermark_node и двумя узлами визуализации изображения отношение один ко многим, поэтому, если узел визуализации изображения использует обратный вызов unique_ptr, невозможно передать владение одним и тем же указателем обоим узлам. Однако его можно передать одному из них. Какой из них получит необработанный указатель, не определено, передается только последний.
Обратите внимание, что узел визуализации изображения не подписан на обратный вызов unique_ptr. Вместо этого используйте подписку const Shared_ptrs. Это означает, что система передает один и тот же общий_ptr в оба обратных вызова. Когда обрабатывается первая подписка в процессе, хранящийся внутри unique_ptr обновляется доshared_ptr. Каждый обратный вызов получит совместное владение одним и тем же сообщением.
Процесс с межпроцессной визуализацией
Еще одна важная вещь — избегать прерываний в процессе нулевого копирования при выполнении межпроцессных подписок. Чтобы проверить это, вы можете запустить первый пример конвейера изображений image_pipeline_all_in_one, а затем запустить автономный экземпляр image_view_node. Результат запуска следующий
Трудно приостановить два изображения одновременно, поэтому изображения могут не совпадать, но важно отметить, что представление изображения image_pipeline_all_in_one показывает один и тот же адрес для каждого шага. Это означает, что нулевая копия в процессе сохраняется, даже если подписано внешнее представление. Вы также можете видеть, что идентификаторы процессов для первых двух строк текста в представлении изображения между процессами и идентификаторы процессов для автономного средства просмотра изображений в третьей строке текста различаются.
Ссылки:
https://docs.ros.org/en/galactic/Tutorials/Demos/Intra-Process-Communication.html