Tutorial 8: Multi-Process Systems (C++)
Real robots run multiple processes — a sensor driver, a controller, a safety monitor, each as a separate binary. This tutorial shows how to build and run multi-process systems.
What You'll Learn
- Separate binaries sharing SHM topics
- Process startup ordering (subscriber before publisher)
- Cross-process service calls
- Using
horus launchfor multi-process orchestration
Architecture
Process 1: lidar_driver Process 2: controller Process 3: safety
┌────────────────────┐ ┌────────────────────┐ ┌────────────┐
│ Publisher │──SHM─→│ Subscriber │ │ Subscriber │
│ "lidar.scan" │ │ "lidar.scan" │ │ "cmd_vel" │
└────────────────────┘ │ Publisher │──SHM─→│ │
│ "cmd_vel" │ └────────────┘
└────────────────────┘
All three processes open the same SHM files in /dev/shm/horus_default/topics/. No message broker, no serialization.
Process 1: Sensor Driver
// sensor_driver.cpp
#include <horus/horus.hpp>
using namespace horus::literals;
class LidarSensor : public horus::Node {
public:
LidarSensor() : Node("lidar") {
pub_ = advertise<horus::msg::LaserScan>("lidar.scan");
}
void tick() override {
auto scan = pub_->loan();
for (int i = 0; i < 360; i++)
scan->ranges[i] = 2.0f + 0.5f * std::sin(i * 0.1f);
pub_->publish(std::move(scan));
}
private:
horus::Publisher<horus::msg::LaserScan>* pub_;
};
int main() {
horus::Scheduler sched;
sched.tick_rate(10_hz).name("lidar_proc");
LidarSensor lidar;
sched.add(lidar).order(0).build();
sched.spin();
}
Process 2: Controller
// controller.cpp
#include <horus/horus.hpp>
using namespace horus::literals;
class Controller : public horus::Node {
public:
Controller() : Node("controller") {
scan_sub_ = subscribe<horus::msg::LaserScan>("lidar.scan");
cmd_pub_ = advertise<horus::msg::CmdVel>("cmd_vel");
}
void tick() override {
auto scan = scan_sub_->recv();
if (!scan) return;
float min_range = 999.0f;
for (int i = 0; i < 360; i++)
if (scan->get()->ranges[i] < min_range)
min_range = scan->get()->ranges[i];
horus::msg::CmdVel cmd{};
cmd.linear = min_range > 0.5f ? 0.3f : 0.0f;
cmd_pub_->send(cmd);
}
void enter_safe_state() override {
horus::msg::CmdVel stop{};
cmd_pub_->send(stop);
}
private:
horus::Subscriber<horus::msg::LaserScan>* scan_sub_;
horus::Publisher<horus::msg::CmdVel>* cmd_pub_;
};
int main() {
horus::Scheduler sched;
sched.tick_rate(50_hz).name("ctrl_proc");
Controller ctrl;
sched.add(ctrl).order(0).build();
sched.spin();
}
Running Multi-Process
# Build both
g++ -std=c++17 -I horus_cpp/include -o sensor sensor_driver.cpp \
-L target/debug -lhorus_cpp -lpthread -ldl -lm
g++ -std=c++17 -I horus_cpp/include -o controller controller.cpp \
-L target/debug -lhorus_cpp -lpthread -ldl -lm
# Run (subscriber process first — creates SHM)
LD_LIBRARY_PATH=target/debug ./controller &
sleep 0.5
LD_LIBRARY_PATH=target/debug ./sensor &
# Monitor from any terminal
horus topic list # shows lidar.scan, cmd_vel
horus node list # shows lidar, controller (separate PIDs)
Startup Order Rule
Subscriber must start before publisher for the first message exchange. The first process to open a topic creates the SHM ring buffer. If the publisher creates it and exits, the data is gone before the subscriber connects.
For production, use horus launch which manages startup ordering automatically.
Key Takeaways
- Each process has its own
Scheduler— they don't share a scheduler - Topics are shared via SHM files (
/dev/shm/horus_default/topics/) - Subscriber should start first to create the ring buffer
horus topic listshows topics from ALL processes- No message broker — direct SHM ring buffer, ~170ns cross-process latency
- Each process can have different tick rates (10 Hz sensor, 50 Hz controller)