concurrencpp Documentation

repository·master·Indexed 25 days ago

https://github.com/david-haim/concurrencpp

A C++ concurrency library providing advanced coroutine support for eager and lazy tasks, alongside a unified executor abstraction for scheduling work across various threading models. It features a runtime manager and multiple executor types, including thread pool, background, thread, worker thread, manual, and inline executors, to decouple task scheduling from application logic.

Tokens
16.9K
Snippets
30
Records
56
Agent score
33%

What's inside concurrencpp

  1. Overview of concurrencpp concepts

    master

    concurrencpp is a C++ concurrency library built around three core concepts:

    • Tasks: Asynchronous operations representing computational steps. They can be implemented using regular callables (lambdas, functors) or C++20 coroutines. Tasks can be suspended and resumed without blocking underlying OS threads.
    • Executors: Worker objects that define where and how tasks are executed (e.g., thread pools). They decouple task scheduling from application logic.
    • Result Objects: Asynchronous pipes used for communication between tasks. They allow tasks to pass results or exceptions to one another in a non-blocking manner.

    The library follows the RAII pattern: a concurrencpp::runtime instance must be created (typically at the start of main) to manage the lifecycle of executors and tasks.

  2. Supported platforms and compilers

    master

    The following environments are supported for concurrencpp:

    Operating Systems:

    • Linux
    • macOS
    • Windows (Windows 10 and above)

    Compilers:

    • MSVC (Visual Studio 2019 version 16.8.2 and above)
    • Clang 14+
    • Clang 11-13 with libc++
    • GCC 13+

    Build Tools:

    • CMake (3.16 and above)
  3. Use eager tasks with coroutines

    master

    concurrencpp supports both eager and lazy tasks via C++20 coroutines:

    • Eager Tasks: Start executing immediately upon invocation. These are ideal for "fire and consume later" or "fire and forget" patterns.
    • Return Types for Eager Tasks:
      • concurrencpp::result<T>: The coroutine passes the returned value or any thrown exception to the caller (fire and consume later).
      • concurrencpp::null_result: The coroutine drops and ignores both the return value and any exceptions (fire and forget).
  4. Use Generators for synchronous value streams

    master

    A generator is a lazy, synchronous coroutine that produces a stream of values using the co_yield keyword.

    Usage Rules:

    • Synchronous only: Generators must not use co_await. They can only use co_yield.
    • Consumption: They are designed to be used in a range-for loop. The loop handles the underlying iterators (begin() and end()) automatically.
    • Termination: A generator stops when co_return is called or an exception is thrown (which is then re-thrown to the consumer).
    • Move-only: Generators are move-only. After moving a generator, it is considered empty. It is recommended to consume them immediately in a for loop.
  5. Build the library on Windows (Release mode)

    master

    To build the concurrencpp library in Release mode on Windows, use CMake to configure the build directory and then build the project.

    $ git clone https://github.com/David-Haim/concurrencpp.git
    $ cd concurrencpp
    $ cmake -S . -B build/lib
    $ cmake --build build/lib --config Release
  6. Run tests on Windows

    master

    To run tests on Windows, configure the test directory and use ctest. You can run in Debug or Release mode by specifying the configuration flag.

    $ git clone https://github.com/David-Haim/concurrencpp.git
    $ cd concurrencpp
    $ cmake -S test -B build/test
    $ cmake --build build/test
        <# for release mode: cmake --build build/test --config Release #>
    $ cd build/test
    $ ctest . -V -C Debug
        <# for release mode: ctest . -V -C Release #>
  7. Monitor thread creation and termination

    master

    You can monitor when concurrencpp workers create or terminate threads by setting callbacks in concurrencpp::runtime_options. These callbacks are executed from within the created or terminating thread, so std::this_thread::get_id() will return the relevant thread ID.

    Callbacks must be copiable because they are copied to each worker. The callback signature is void callback(std::string_view thread_name), where thread_name is a non-unique title used for logging and debugging.

    #include "concurrencpp/concurrencpp.h"
    #include <iostream>
    
    int main() {
        concurrencpp::runtime_options options;
        options.thread_started_callback = [](std::string_view thread_name) {
            std::cout << "A new thread is starting to run, name: " << thread_name << ", thread id: " << std::this_thread::get_id()
                      << std::endl;
        };
    
        options.thread_terminated_callback = [](std::string_view thread_name) {
            std::cout << "A thread is terminating, name: " << thread_name << ", thread id: " << std::this_thread::get_id() << std::endl;
        };
    
        concurrencpp::runtime runtime(options);
        // ... rest of application
    }
  8. Initialize and use the concurrencpp runtime

    master

    To use the library, instantiate a concurrencpp::runtime object. This object manages the lifecycle of all executors. When the runtime object is destroyed, it automatically calls shutdown on all stored executors, ensuring a graceful exit. You use the runtime instance to acquire existing executors (like the thread executor or thread pool executor) or to register new user-defined executors.

    Example of a basic "Hello World" using the runtime and thread executor:

    #include "concurrencpp/concurrencpp.h"
    #include <iostream>
    
    int main() {
        concurrencpp::runtime runtime;
        auto result = runtime.thread_executor()->submit([] {
            std::cout << "hello world" << std::endl;
        });
    
        result.get();
        return 0;
    }
  9. Understand Lazy vs Eager Tasks

    master

    Concurrencpp distinguishes between two main types of coroutines/tasks:

    • Eager Coroutines: Can start running synchronously in the caller thread ("regular coroutines") or in parallel inside an executor ("parallel coroutines").
    • Lazy Tasks: Start running only when co_awaited. They are optimized for immediate consumption and require less thread-synchronization and memory allocation. However, firing a lazy task suspends the caller until the task completes.

    Conversion: You can convert a lazy_result to an eager task by calling lazy_result::run. This runs the task inline and returns a result object that monitors the task. If unsure, it is recommended to use lazy_result as it can be converted to eager results if needed.

  10. Initialize and manage the concurrencpp runtime

    master

    The runtime object is the central agent for managing executors and the global timer queue.

    Lifecycle Management

    • Creation: Create a runtime object as a value type at the very beginning of your main function. This ensures no tasks are processed before the runtime exists.
    • Destruction (RAII): When the runtime object goes out of scope, it automatically shuts down all registered executors and the timer queue.
    • Termination: Ongoing tasks should exit as soon as possible during shutdown. If a task tries to use an executor after the runtime has begun shutting down, a concurrencpp::runtime_shutdown exception will be thrown.

    Executor Acquisition

    Use the runtime object to obtain standard executors:

    • thread_pool_executor(): A standard thread pool.
    • inline_executor(): Executes tasks immediately on the current thread.
    • background_executor(): A background thread pool.
    • thread_executor(): A single-threaded executor.

    Custom Executors

    You can register custom executors with the runtime using:

    • make_worker_thread_executor()
    • make_manual_executor()
    • make_executor<executor_type>(args...)
  11. Create user-defined executors

    master

    To implement a custom executor, inherit from concurrencpp::derivable_executor<T>.

    Key Requirements:

    1. Thread Safety: Executors are used from multiple threads; all implemented methods must be thread-safe.
    2. Instantiation: Use runtime::make_executor<T>(args...) to create new executors. Do not use std::make_shared, new, or attempt to re-instantiate built-in executors like thread_pool_executor.
    3. Shutdown Handling:
      • shutdown(): Must signal underlying threads to quit and then join them. It must handle multiple calls by ignoring subsequent calls after the first.
      • shutdown_requested(): Should monitor the executor state.
      • enqueue(): Must throw concurrencpp::errors::runtime_shutdown if shutdown() has already been called.
    4. Task Storage: Implementations are responsible for storing concurrencpp::task objects received via enqueue and executing them according to the executor's mechanism.
    #include "concurrencpp/concurrencpp.h"
    #include <iostream>
    #include <queue>
    #include <thread>
    #include <mutex>
    #include <condition_variable>
    
    class logging_executor : public concurrencpp::derivable_executor<logging_executor> {
    private:
        mutable std::mutex _lock;
        std::queue<concurrencpp::task> _queue;
        std::condition_variable _condition;
        bool _shutdown_requested;
        std::thread _thread;
        const std::string _prefix;
    
        void work_loop() {
            while (true) {
                std::unique_lock<std::mutex> lock(_lock);
                if (_shutdown_requested) return;
    
                if (!_queue.empty()) {
                    auto task = std::move(_queue.front());
                    _queue.pop();
                    lock.unlock();
                    std::cout << _prefix << " A task is being executed" << std::endl;
                    task();
                    continue;
                }
    
                _condition.wait(lock, [this] { return !_queue.empty() || _shutdown_requested; });
            }
        }
    
    public:
        logging_executor(std::string_view prefix) :
            derivable_executor<logging_executor>("logging_executor"),
            _shutdown_requested(false),
            _prefix(prefix) {
            _thread = std::thread([this] { work_loop(); });
        }
    
        void enqueue(concurrencpp::task task) override {
            std::cout << _prefix << " A task is being enqueued!" << std::endl;
            std::unique_lock<std::mutex> lock(_lock);
            if (_shutdown_requested) {
                throw concurrencpp::errors::runtime_shutdown("logging executor - executor was shutdown.");
            }
            _queue.emplace(std::move(task));
            _condition.notify_one();
        }
    
        void enqueue(std::span<concurrencpp::task> tasks) override {
            std::cout << _prefix << tasks.size() << " tasks are being enqueued!" << std::endl;
            std::unique_lock<std::mutex> lock(_lock);
            if (_shutdown_requested) {
                throw concurrencpp::errors::runtime_shutdown("logging executor - executor was shutdown.");
            }
            for (auto& task : tasks) {
                _queue.emplace(std::move(task));
            }
            _condition.notify_one();
        }
    
        int max_concurrency_level() const noexcept override { return 1; }
    
        bool shutdown_requested() const noexcept override {
            std::unique_lock<std::mutex> lock(_lock);
            return _shutdown_requested;
        }
    
        void shutdown() noexcept override {
            std::cout << _prefix << " shutdown requested" << std::endl;
            std::unique_lock<std::mutex> lock(_lock);
            if (_shutdown_requested) return;
            _shutdown_requested = true;
            lock.unlock();
            _condition.notify_one();
            _thread.join();
        }
    };
    
    int main() {
        concurrencpp::runtime runtime;
        auto logging_ex = runtime.make_executor<logging_executor>("Session #1234");
    
        for (size_t i = 0; i < 10; i++) {
            logging_ex->post([] { std::cout << "hello world" << std::endl; });
        }
    
        std::getchar();
        return 0;
    }