Concurrence Demo (C++)
This tutorial demonstrates how to run multiple states concurrently in
C++ using YASMIN's Concurrence container. Concurrency is
essential in robotics for coordinating parallel operations like
simultaneously monitoring sensors while executing actions, waiting for
multiple conditions to be met, or performing independent tasks in
parallel. The Concurrence container manages thread synchronization,
outcome resolution, and blackboard access automatically, making it
safe and easy to implement parallel state execution.
Background
The Concurrence container enables parallel execution of multiple states. It is useful when you need to:
- Execute independent tasks simultaneously without blocking each other
- Wait for multiple conditions to be met (AND/OR logic)
- Coordinate parallel behaviors with custom outcome logic
- Implement timeout patterns where one state monitors time while another performs work
- Monitor sensors while executing actions (e.g., watch for obstacles while navigating)
1. Create the FooState
This state increments a counter and returns different outcomes based
on the counter value. It executes relatively quickly (2 seconds) and
stores a formatted string in the blackboard. The three different
outcomes ("outcome1", "outcome2",
"outcome3") demonstrate how concurrent states can
progress through stages and produce varying results. The counter is
incremented after determining the outcome, so the state's behavior
evolves with each execution. This pattern is useful for tasks that
have distinct phases or thresholds, like collecting sensor samples,
performing iterations of a computation, or tracking progress toward a
goal.
class FooState : public yasmin::State {
public:
int counter;
FooState()
: yasmin::State({"outcome1", "outcome2", "outcome3"}), counter(0) {};
std::string
execute(std::shared_ptr<yasmin::blackboard::Blackboard> blackboard) override {
YASMIN_LOG_INFO("Executing state FOO");
std::this_thread::sleep_for(std::chrono::seconds(2));
std::string outcome;
blackboard->set<std::string>("foo_str",
"Counter: " + std::to_string(this->counter));
if (this->counter < 3) {
outcome = "outcome1";
} else if (this->counter < 5) {
outcome = "outcome2";
} else {
outcome = "outcome3";
}
YASMIN_LOG_INFO("Finishing state FOO");
this->counter += 1;
return outcome;
};
};
2. Create the BarState
Define a slower state (4 seconds) that checks if FooState has written
to the blackboard. This demonstrates a key aspect of concurrent
execution: timing matters. Since BarState takes longer than FooState,
there's a race condition - sometimes BarState will execute before
FooState has written foo_str to the blackboard. The
contains() check safely handles this by checking if the
key exists before accessing it. This pattern is crucial in concurrent
systems where you need to handle asynchronous data availability.
BarState always returns "outcome3", providing a
consistent counterpoint to FooState's variable outcomes.
class BarState : public yasmin::State {
public:
BarState() : yasmin::State({"outcome3"}) {}
std::string
execute(std::shared_ptr<yasmin::blackboard::Blackboard> blackboard) override {
YASMIN_LOG_INFO("Executing state BAR");
std::this_thread::sleep_for(std::chrono::seconds(4));
if (blackboard->contains("foo_str")) {
YASMIN_LOG_INFO(blackboard->get<std::string>("foo_str").c_str());
} else {
YASMIN_LOG_INFO("blackboard does not yet contains 'foo_str'");
}
YASMIN_LOG_INFO("Finishing state BAR");
return "outcome3";
}
};
3. Create Concurrence Container
Create a Concurrence container that runs FooState and BarState in
parallel. The constructor takes three parameters: a map of state names
to state objects, a default outcome (returned when no outcome map
matches), and an outcome map that defines how concurrent results map
to container outcomes. The outcome map specifies: if FOO returns
"outcome1" AND BAR returns "outcome3", the
concurrence returns "outcome1"; if FOO returns
"outcome2" AND BAR returns "outcome3",
return "outcome2". If neither condition matches (e.g.,
FOO returns "outcome3"), the concurrence returns the
default outcome "defaulted". This flexible outcome
mapping allows you to implement AND, OR, or custom logic for combining
parallel state results.
int main(int argc, char *argv[]) {
YASMIN_LOG_INFO("yasmin_concurrence_demo");
rclcpp::init(argc, argv);
yasmin_ros::set_ros_loggers();
auto sm = std::make_shared<yasmin::StateMachine>(
std::initializer_list<std::string>{"outcome4"});
// Cancel state machine on ROS 2 shutdown
rclcpp::on_shutdown([sm]() {
if (sm->is_running()) {
sm->cancel_state();
}
});
auto foo_state = std::make_shared<FooState>();
auto bar_state = std::make_shared<BarState>();
auto concurrent_state = std::make_shared<yasmin::Concurrence>(
std::map<std::string, std::shared_ptr<yasmin::State>>{
{"FOO", foo_state},
{"BAR", bar_state}
},
"defaulted",
yasmin::Concurrence::OutcomeMap{
{"outcome1",
yasmin::Concurrence::StateOutcomeMap{{"FOO", "outcome1"},
{"BAR", "outcome3"}}},
{"outcome2", yasmin::Concurrence::StateOutcomeMap{
{"FOO", "outcome2"}, {"BAR", "outcome3"}}}
});
4. Add to State Machine
Add the concurrence container to the state machine with self-loops for
outcome1 and outcome2, allowing it to
execute multiple times. When the default outcome
("defaulted") is reached, the state machine exits with
outcome4. This setup enables the demonstration to run
through several concurrent executions before terminating.
sm->add_state("CONCURRENCE", concurrent_state,
{
{"outcome1", "CONCURRENCE"},
{"outcome2", "CONCURRENCE"},
{"defaulted", "outcome4"},
});
yasmin_viewer::YasminViewerPub yasmin_pub(sm, "YASMIN_CONCURRENCE_DEMO");
try {
std::string outcome = (*sm.get())();
YASMIN_LOG_INFO(outcome.c_str());
} catch (const std::exception &e) {
YASMIN_LOG_WARN(e.what());
}
rclcpp::shutdown();
5. Run the Demo
Execute the concurrence demonstration to observe parallel state execution and outcome resolution.
ros2 run yasmin_demos concurrence_demo
YASMIN