The Boson Framework is a C++ library to write concurrent software in C++. The framework inspires freely from what the Go language did to address complexity in concurrent programming. The Boson Framework implements features similar to three of Go's:
- The Go routines
- The net poller
- The channels
This project is currently a proof of concept. New features and performance improvements are to be expected in the near future.
The Boson Framework only supports Linux on x86-64 platforms for now, but Windows and Mac OS are in the roadmap.
The main point is to simplify writing concurrent applications by removing the asynchronous logic from the developer's mind. The developer only uses blocking calls, and the whole logic is written sequentially. Under the hood, blocking calls are not blocking from the OS standpoint, since the thread is still used to execute some tasks. This is different from using multiple threads because:
- Routines (or fibers) are lightweight compared to threads, to launch and to schedule.
- Interruption points are known by the developer, whereas they are not when scheduling threads.
- Ease of use: The learning curve is very gentle, especially for
C
andGo
developers used to system interfaces. - Select statement: The
select_*
statement is versatile and allows programming constructs that are hard to reproduce without it. - System calls interception: Libraries that have not been prepared to can be used in a asynchronized way with a
LD_PRELOAD
trick.
The goal of the framework is to use light routines instead of threads. In Go, these are called Goroutines. In the C++ world, it is known as fibers. In the boson framework, we just call them routines.
This snippet just launches a simple routine that exits immediately.
boson::run(1 /* number of thread */, []() {
std::cout << "Hello world" << std::endl;
});
The boson framework provides its versions of system calls that are scheduled away for efficiency with an event loop. Current asynchronized syscalls are:
sleep
,usleep
,nanosleep
read
,recv
write
,send
accept
connect
See an example.
This snippet launches two routines doing different jobs, in a single thread.
void timer(int out, bool& stopper) {
while(!stopper) {
boson::sleep(1000ms);
boson::write(out,"Tick !\n",7);
}
boson::write(out,"Stop !\n",7);
}
void user_input(int in, bool& stopper) {
char buffer[1];
boson::read(in, &buffer, sizeof(buffer));
stopper = true;
}
int main(int argc, char *argv[]) {
bool stopper = false;
boson::run(1, [&stopper]() {
boson::start(timer, 1, stopper);
::fcntl(0, F_SETFL, ::fcntl(0, F_GETFD) | O_NONBLOCK);
boson::start(user_input, 0, stopper);
});
}
This executable prints Tick !
every second and quits if the user enters anything on the standard input. We dont have to manage concurrency on the stopper
variable since we know only one thread uses it. Plus, we know at which points the routines might be interrupted.
The boson framework implements channels similar to Go ones. Channels are used to communicate between routines. They can be used over multiple threads.
This snippet listens to the standard input in one thread and writes to two different files in two other threads. A channel is used to communicate. This also demonstrates the use of a generic lambda and a generic functor. Boson system calls are not used on the files because files on disk cannot be polled for events (not allowed by the Linux kernel).
struct writer {
template <class Channel>
void operator()(Channel input, char const* filename) const {
std::ofstream file(filename);
if (file) {
std::string buffer;
while(input >> buffer)
file << buffer << std::flush;
}
}
};
int main(int argc, char *argv[]) {
boson::run(3, []() {
boson::channel<std::string, 1> pipe;
// Listen stdin
boson::start_explicit(0, [](int in, auto output) -> void {
char buffer[2048];
ssize_t nread = 0;
while(0 < (nread = ::read(in, &buffer, sizeof(buffer)))) {
output << std::string(buffer,nread);
std::cout << "iter" << std::endl;
}
output.close();
}, 0, pipe);
// Output in files
writer functor;
boson::start_explicit(1, functor, pipe, "file1.txt");
boson::start_explicit(2, functor, pipe, "file2.txt");
});
}
Channels must be transfered by copy. Generic lambdas, when used as a routine seed, must explicitely state that they return void
. The why will be explained in detail in further documentation. Threads are assigned to routines in a round-robin fashion. The thread id can be explicitely given when starting a routine.
boson::start_explicit(0, [](int in, auto output) -> void {...}, 0, pipe);
boson::start_explicit(1, functor, pipe, "file1.txt");
boson::start_explicit(2, functor, pipe, "file2.txt");
See an example.
The select statement is similar to the Go one, but with a nice twist : it can be used with any blocking facility. That means you can mix channels, i/o events, mutex locks and timers in a single select_*
call.
// Create a pipe
int pipe_fds[2];
::pipe(pipe_fds);
::fcntl(pipe_fds[0], F_SETFL, ::fcntl(pipe_fds[0], F_GETFD) | O_NONBLOCK);
::fcntl(pipe_fds[1], F_SETFL, ::fcntl(pipe_fds[1], F_GETFD) | O_NONBLOCK);
boson::run(1, [&]() {
using namespace boson;
// Create channel
channel<int, 3> chan;
// Create mutex and lock it immediately
boson::mutex mut;
mut.lock();
// Start a producer
start([](int out, auto chan) -> void {
int data = 1;
boson::write(out, &data, sizeof(data));
chan << data;
}, pipe_fds[1], chan);
// Start a consumer
start([](int in, auto chan, auto mut) -> void {
int buffer = 0;
bool stop = false;
while (!stop) {
select_any( //
event_read(in, &buffer, sizeof(buffer),
[](ssize_t rc) { //
std::cout << "Got data from the pipe \n";
}),
event_read(chan, buffer,
[](bool) { //
std::cout << "Got data from the channel \n";
}),
event_lock(mut,
[]() { //
std::cout << "Got lock on the mutex \n";
}),
event_timer(100ms,
[&stop]() { //
std::cout << "Nobody loves me anymore :(\n";
stop = true;
}));
}
}, pipe_fds[0], chan, mut);
// Start an unlocker
start([](auto mut) -> void {
mut.unlock();
}, mut);
});
select_any
can return a value :
int result = select_any( //
event_read(in, &buffer, sizeof(buffer), [](ssize_t rc) { return 1; }), //
event_read(chan, buffer, [](bool) { return 2; }), //
event_timer(100ms, []() { return 3; })); //
switch(result) {
case 1:
std::cout << "Got data from the pipe \n";
break;
case 2:
std::cout << "Got data from the channel \n";
break;
default:
std::cout << "Nobody loves me anymore :(\n";
stop = true;
break;
}
See an example.
Head to the examples to see more code.
Please refer to the documentation.
Find the manual here.
The boson framework is distributed under the terms of The MIT License. Third parties are distributed under the terms of their own licenses. Third party code is the 3rdparty
directory.