Introduction to SObjectizer
SObjectizer is a cross-platform, open-source actor framework designed for C++ development. It extends beyond the traditional Actor Model to incorporate Publish-Subscribe and CSP-like channels, aiming to significantly simplify the process of creating concurrent and multithreaded applications in C++. By structuring an application as a collection of agent objects that communicate via asynchronous messages, SObjectizer efficiently manages message dispatching and processing contexts, catering to the need for streamlined concurrent computability.
What Sets SObjectizer Apart
Maturity
SObjectizer is rooted in concepts dating back to 1995-2000, with its development commencing in 2002. The current version, SObjectizer-5, has seen continuous evolution since 2010.
Stability
The framework has been employed for business-critical applications from its inception. It features a stable API with minimal breaking changes, ensuring reliability for production applications still in use today.
Cross-Platform
SObjectizer is compatible with various operating systems, including Windows, Linux, FreeBSD, macOS, and Android.
User-Friendly
The framework offers an intuitive API complemented by numerous examples and extensive documentation available in its distribution and on its project Wiki.
Free Licensing
SObjectizer is distributed under the BSD-3-CLAUSE license, allowing free usage for proprietary commercial software development.
Different from TBB, Taskflow, and HPX
While often compared to Intel Threading Building Blocks, taskflow, and HPX, SObjectizer occupies a distinct niche in Concurrent Computing, focusing on handling multiple simultaneous tasks. It employs the same concurrency mechanisms as Parallel Computing tools but is tailored for diverse task execution, often not requiring multicore capability.
Code Examples
HelloWorld Example
A basic "Hello, World" program using SObjectizer illustrates its simplicity:
#include <so_5/all.hpp>
class hello_actor final : public so_5::agent_t {
public:
using so_5::agent_t::agent_t;
void so_evt_start() override {
std::cout << "Hello, World!" << std::endl;
so_deregister_agent_coop_normally();
}
};
int main() {
so_5::launch([](so_5::environment_t & env) {
env.register_agent_as_coop(env.make_agent<hello_actor>());
});
return 0;
}
Ping-Pong Example
This example simulates the classic Ping-Pong message exchange between two agents:
#include <so_5/all.hpp>
struct ping { int counter_; };
struct pong { int counter_; };
class pinger final : public so_5::agent_t {
so_5::mbox_t ponger_;
public:
pinger(context_t ctx) : so_5::agent_t{std::move(ctx)} {}
void set_ponger(const so_5::mbox_t mbox) { ponger_ = mbox; }
void so_define_agent() override {
so_subscribe_self().event(&pinger::on_pong);
}
void so_evt_start() override {
so_5::send<ping>(ponger_, 1000);
}
private:
void on_pong(mhood_t<pong> cmd) {
if(cmd->counter_ > 0)
so_5::send<ping>(ponger_, cmd->counter_ - 1);
else
so_deregister_agent_coop_normally();
}
};
class ponger final : public so_5::agent_t {
const so_5::mbox_t pinger_;
int pings_received_{};
public:
ponger(context_t ctx, so_5::mbox_t pinger)
: so_5::agent_t{std::move(ctx)}, pinger_{std::move(pinger)} {}
void so_define_agent() override {
so_subscribe_self().event([this](mhood_t<ping> cmd) {
++pings_received_;
so_5::send<pong>(pinger_, cmd->counter_);
});
}
void so_evt_finish() override {
std::cout << "pings received: " << pings_received_ << std::endl;
}
};
int main() {
so_5::launch([](so_5::environment_t & env) {
env.introduce_coop([](so_5::coop_t & coop) {
auto pinger_actor = coop.make_agent<pinger>();
auto ponger_actor = coop.make_agent<ponger>(
pinger_actor->so_direct_mbox());
pinger_actor->set_ponger(ponger_actor->so_direct_mbox());
});
});
return 0;
}
Pub/Sub Example
Demonstrating the Publish-Subscribe model in SObjectizer, this example shows message distribution to multiple subscribers.
#include <so_5/all.hpp>
struct acquired_value {
std::chrono::steady_clock::time_point acquired_at_;
int value_;
};
class producer final : public so_5::agent_t {
const so_5::mbox_t board_;
so_5::timer_id_t timer_;
int counter_{};
public:
producer(context_t ctx, so_5::mbox_t board)
: so_5::agent_t{std::move(ctx)}, board_{std::move(board)} {}
void so_define_agent() override {
so_subscribe_self().event(&producer::on_timer);
}
void so_evt_start() override {
timer_ = so_5::send_periodic<acquisition_time>(*this, 0ms, 750ms);
}
private:
void on_timer(mhood_t<acquisition_time>) {
so_5::send<acquired_value>(
board_, std::chrono::steady_clock::now(), ++counter_);
}
struct acquisition_time final : public so_5::signal_t {};
};
class consumer final : public so_5::agent_t {
const so_5::mbox_t board_;
const std::string name_;
public:
consumer(context_t ctx, so_5::mbox_t board, std::string name)
: so_5::agent_t{std::move(ctx)}, board_{std::move(board)}, name_{std::move(name)} {}
void so_define_agent() override {
so_subscribe(board_).event(&consumer::on_value);
}
void on_value(mhood_t<acquired_value> cmd) {
std::cout << name_ << ": " << cmd->value_ << std::endl;
}
};
int main() {
so_5::launch([](so_5::environment_t & env) {
auto board = env.create_mbox();
env.introduce_coop([board](so_5::coop_t & coop) {
coop.make_agent<producer>(board);
coop.make_agent<consumer>(board, "first"s);
coop.make_agent<consumer>(board, "second"s);
});
std::this_thread::sleep_for(std::chrono::seconds(4));
env.stop();
});
return 0;
}
Additional Features in so5extra
The companion project, so5extra, offers additional features like Asio's based dispatchers, new mbox types, revocable timers, and synchronous requests, among others, extending SObjectizer's capabilities further.
Limitations
While robust, SObjectizer focuses on in-process message dispatching, which may impose constraints depending on the specific use-case.
This comprehensive overview captures the essence of SObjectizer, highlighting its core functionalities, distinguishing features, and the substantial value it offers in the realm of concurrent C++ programming.