Cbeam: Unmatched Concurrency, Cross-Library Data Sharing, and More in Modern C++
Introduction Cbeam is a modern, cross-platform, header-only C++ library designed to simplify concurrency, memory sharing, serialization, and more—without depending on Boost. It draws inspiration from Boost’s coding style but remains lightweight and easy to integrate on Linux, macOS, and Windows. Four standout features illustrate Cbeam’s capabilities: Asynchronous Message Dispatching (using message_manager) Cross-Platform Shared Memory (using interprocess_shared_memory) Stable Reference Counting Across Shared Libraries (using stable_reference_buffer) Interoperable Containers Across Compiler Boundaries (with “stable interprocess” containers) We’ll take a closer look at message_manager and show how it enables straightforward, thread-safe message handling. But first, here’s a quick overview of the other three “spotlight features”: Cross-Platform Shared Memory: interprocess_shared_memory unifies Windows (native file mapping) and Unix (POSIX shm_open) shared memory under a simple API. No leftover OS objects to clean up and no complicated file permissions for C:\ProgramData\boost_interprocess. Stable Reference Counting: stable_reference_buffer places the reference count in a safe, process-wide map, solving the classic issue where std::shared_ptr across shared library boundaries can lead to mismatched counts and double frees. Interoperable Containers: “stable interprocess” containers (like stable_interprocess_map) allow sharing data between libraries or processes compiled with different toolchains. Serialization logic handles potential ABI mismatches. You might also explore nested_map for hierarchical data (configuration trees, JSON-like structures) if your application needs more flexible, nested storage. Deep Dive: Asynchronous Message Dispatching with message_manager Core Idea The Cbeam message_manager helps you send messages to one or more handler threads. Each message type is identified by a numeric ID, and each ID can be associated with one or multiple handlers. Internally, message_manager maintains safe queues, providing FIFO, FILO, or RANDOM dispatch order. It includes mechanisms to wait until all messages for a given ID have been processed, which is especially useful for clean synchronization points. Why Not Just Use std::thread and std::condition_variable? Manual threading requires you to handle queueing, synchronization, and error handling. With message_manager, you reduce boilerplate and minimize risks like deadlocks or race conditions. It’s header-only, has no external dependencies, and works cross-platform, adding minimal performance overhead while significantly reducing the risk of concurrency bugs. In addition, message_manager can integrate with Cbeam’s serialization features to pass data across shared libraries that may not be ABI-compatible. Step-by-Step Example Below is a simplified prime-checking program to illustrate the design. We define two message IDs: check_prime: A handler that checks if a number is prime and, if so, sends it to the second handler. count_prime: A handler that increments a shared counter each time it receives a “prime” message. We enqueue numbers from 1 to 100,000, wait for all tasks to finish, then dispose the handlers. #include #include #include #include // Quick prime test static bool is_prime(uint64_t n) { if (n
Introduction
Cbeam is a modern, cross-platform, header-only C++ library designed to simplify concurrency, memory sharing, serialization, and more—without depending on Boost. It draws inspiration from Boost’s coding style but remains lightweight and easy to integrate on Linux, macOS, and Windows. Four standout features illustrate Cbeam’s capabilities:
-
Asynchronous Message Dispatching (using
message_manager
) -
Cross-Platform Shared Memory (using
interprocess_shared_memory
) -
Stable Reference Counting Across Shared Libraries (using
stable_reference_buffer
) - Interoperable Containers Across Compiler Boundaries (with “stable interprocess” containers)
We’ll take a closer look at message_manager
and show how it enables straightforward, thread-safe message handling. But first, here’s a quick overview of the other three “spotlight features”:
-
Cross-Platform Shared Memory:
interprocess_shared_memory
unifies Windows (native file mapping) and Unix (POSIXshm_open
) shared memory under a simple API. No leftover OS objects to clean up and no complicated file permissions forC:\ProgramData\boost_interprocess
. -
Stable Reference Counting:
stable_reference_buffer
places the reference count in a safe, process-wide map, solving the classic issue wherestd::shared_ptr
across shared library boundaries can lead to mismatched counts and double frees. -
Interoperable Containers: “stable interprocess” containers (like
stable_interprocess_map
) allow sharing data between libraries or processes compiled with different toolchains. Serialization logic handles potential ABI mismatches.
You might also explore nested_map
for hierarchical data (configuration trees, JSON-like structures) if your application needs more flexible, nested storage.
Deep Dive: Asynchronous Message Dispatching with message_manager
Core Idea
The Cbeam message_manager
helps you send messages to one or more handler threads. Each message type is identified by a numeric ID, and each ID can be associated with one or multiple handlers. Internally, message_manager
maintains safe queues, providing FIFO, FILO, or RANDOM dispatch order. It includes mechanisms to wait until all messages for a given ID have been processed, which is especially useful for clean synchronization points.
Why Not Just Use std::thread
and std::condition_variable
?
Manual threading requires you to handle queueing, synchronization, and error handling. With message_manager
, you reduce boilerplate and minimize risks like deadlocks or race conditions. It’s header-only, has no external dependencies, and works cross-platform, adding minimal performance overhead while significantly reducing the risk of concurrency bugs. In addition, message_manager
can integrate with Cbeam’s serialization features to pass data across shared libraries that may not be ABI-compatible.
Step-by-Step Example
Below is a simplified prime-checking program to illustrate the design. We define two message IDs:
-
check_prime
: A handler that checks if a number is prime and, if so, sends it to the second handler. -
count_prime
: A handler that increments a shared counter each time it receives a “prime” message.
We enqueue numbers from 1 to 100,000, wait for all tasks to finish, then dispose the handlers.
#include
#include
#include
#include
// Quick prime test
static bool is_prime(uint64_t n) {
if (n < 2) return false;
if (n % 2 == 0) return (n == 2);
for (uint64_t i = 3, stop = static_cast<uint64_t>(std::sqrt(n)); i <= stop; i += 2) {
if (n % i == 0) return false;
}
return true;
}
constexpr size_t check_prime = 1;
constexpr size_t count_prime = 2;
int main() {
// 1) Create a message_manager that handles messages carrying a
// 64-bit integer. (Side note: message_manager can integrate
// with serialization to pass data across shared libraries
// that might not be ABI-compatible.)
cbeam::concurrency::message_manager<uint64_t> mm;
std::atomic<size_t> prime_count{0};
// 2) Define a handler that increments the prime count.
// (Note that we ignore the actual number here — only the
// event matters.)
mm.add_handler(count_prime, [&](uint64_t) {
++prime_count;
});
// 3) Define a handler to check numbers and forward prime ones
mm.add_handler(check_prime, [&](uint64_t number) {
if (is_prime(number)) {
mm.send_message(count_prime, number);
}
});
// (You could register multiple add_handler calls with
// the same ID to distribute the work across multiple
// threads - just run the above code `n` times.)
// 4) Enqueue many numbers for checking
for (uint64_t i = 1; i <= 100000; ++i) {
mm.send_message(check_prime, i);
}
// 5) Wait for all messages to be processed
mm.wait_until_empty(check_prime);
mm.wait_until_empty(count_prime);
// 6) Dispose the handlers
mm.dispose(check_prime);
mm.dispose(count_prime);
// Print the total primes found
std::cout << "Total primes in [1..100000]: " << prime_count.load() << std::endl;
return 0;
}
Highlights of message_manager
-
Multiple Handlers per Message ID: Each
add_handler
spawns an independent thread. If you want parallel processing for a single message ID, simply register multiple handlers for that ID. - Ordering Options: By default, messages are queued in FIFO order, but you can choose FILO or RANDOM if needed for particular algorithms.
-
Cleaner Synchronization:
wait_until_empty(ID)
ensures all tasks in that queue finish, letting you safely finalize results.
Feedback and Discussion
Interested in trying Cbeam? We welcome feedback, ideas, and discussion on our Discord server. Whether you’re tackling concurrency challenges, bridging incompatible compilers, or simplifying shared memory usage, Cbeam aims to offer robust solutions under a permissive, modern C++ approach.
Check out Cbeam.org for detailed Doxygen docs, unit tests, and more examples to help streamline your next cross-platform C++ project.