C++20 header-only library for parallel computing: coroutines, executors, SIMD abstractions, lock-free data structures, and parallel algorithms.
Add the library to your project using CMake and FetchContent:
cmake_minimum_required(VERSION 3.16)
project(MY_PROJECT LANGUAGES CXX)
include(FetchContent)
FetchContent_Declare(
pot
GIT_REPOSITORY https://github.com/esufaev/Parallel-Operations-Toolkit.git
GIT_TAG master
)
FetchContent_MakeAvailable(pot)
add_executable(MY_PROJECT main.cpp)
target_link_libraries(MY_PROJECT PRIVATE pot::pot)To include everything at once:
#include "pot/pot.h"Starts execution immediately upon creation.
#include "pot/coroutines/task.h"
pot::coroutines::task<int> compute() { co_return 42; }
int main() {
auto t = compute();
std::cout << "Result: " << t.get() << "\n";
}Nothing executes until get() is called.
#include "pot/coroutines/task.h"
pot::coroutines::lazy_task<int> delayed() {
std::cout << "Started computation\n";
co_return 7;
}
int main() {
auto lt = delayed(); // nothing happens
std::cout << "Before get()\n";
std::cout << "Value: " << lt.get() << "\n"; // launches the coroutine
}Output:
Before get() Started computation Value: 7
Executors (executor) provide an abstraction for running functions or coroutines on different execution strategies.
All executors implement the pot::executor interface and can be used interchangeably.
run_detached(func, args...)— fire and forget.run(func, args...)— run and return an eagertask.lazy_run(func, args...)— run and return alazy_task.shutdown()— stop the executor.thread_count()— number of worker threads.
Runs tasks synchronously in the current thread. Useful for testing and debugging.
pot::executors::inline_executor exec("inline");
exec.run_detached([] {
std::cout << "Runs in the same thread" << std::endl;
});Allocates a dedicated thread for all submitted tasks.
pot::executors::thread_executor exec("single-thread");
exec.run_detached([] {
std::cout << "Runs in the dedicated thread" << std::endl;
});
exec.shutdown();The library provides several thread pool strategies, differing in queue structure and load balancing:
| Class | Queue | Topology | Work stealing |
|---|---|---|---|
thread_pool_executor_gq |
std::queue + mutex |
Global | No |
thread_pool_executor_lq |
std::queue + mutex |
Local (round-robin) | No |
thread_pool_executor_lfgq |
Lock-free queue | Global | No |
thread_pool_executor_lflq |
Lock-free queue | Local (round-robin) | No |
All pools default to std::thread::hardware_concurrency() threads.
pot::executors::thread_pool_executor_lfgq pool("pool", 12);
for (int i = 0; i < 10; ++i) {
pool.run_detached([i] {
std::cout << "Task " << i << " in thread pool" << std::endl;
});
}
pool.shutdown();Asynchronous parallel for loop. Automatically splits the iteration range into chunks and executes them on the given executor.
template <int64_t static_chunk_size = -1, typename IndexType, typename FuncType = void(IndexType)>
requires std::invocable<FuncType &, IndexType>
pot::coroutines::lazy_task<void>
parfor(pot::executor &executor, IndexType from, IndexType to, FuncType&& func);| Parameter | Type | Description |
|---|---|---|
static_chunk_size |
int64_t (default -1) |
Chunk size. If < 0, computed automatically. |
executor |
pot::executor& |
Executor for parallelization. |
from |
IndexType |
Start index (inclusive). |
to |
IndexType |
End index (exclusive). |
func |
callable | Function for each iteration. Supports both synchronous and coroutine functions. |
pot::coroutines::lazy_task<void> — completes when all parallel tasks finish.
Synchronous function:
#include "pot/algorithms/parfor.h"
#include "pot/executors/thread_pool_executor.h"
void example_sync() {
pot::thread_pool_executor exec(4);
parfor(exec, 0, 100, [](int i) {
printf("Index: %d\n", i);
}).get();
}Asynchronous function (coroutine):
pot::coroutines::task<void> process_item(int i) {
co_await some_async_operation(i);
}
void example_async() {
pot::thread_pool_executor exec(4);
parfor(exec, 0, 100, [](int i) -> pot::coroutines::task<void> {
co_await process_item(i);
}).sync_wait();
}Runs multiple independent sections in parallel. Completes when all sections finish.
pot::coroutines::task<void> coroA();
pot::coroutines::lazy_task<void> coroB();
co_await pot::algorithms::parsections(exec,
[] { prepare(); },
[]() -> pot::coroutines::task<void> { co_await coroA(); co_return; },
[]() -> pot::coroutines::lazy_task<void> { co_await coroB(); co_return; }
);Combinator that completes when all provided awaitables finish.
// Variadic
co_await pot::coroutines::when_all(a(), b(), c());
// Container
std::vector<pot::coroutines::task<void>> tasks;
tasks.push_back(do_work(1));
tasks.push_back(do_work(2));
co_await pot::coroutines::when_all(tasks);
// Iterator range
co_await pot::coroutines::when_all(tasks.begin(), tasks.end());Asynchronous element-wise reduction over two arrays. Applies a binary operation (elem_op(a[i], b[i])) to each pair, then reduces the results.
// Dot product
co_return co_await pot::algorithms::elementwise_reduce<double, double>(
exec, a, b,
std::multiplies<double>{},
std::plus<double>{},
0.0
);SIMD variant: processes multiple elements per iteration using simd_forced, then reduces the tail scalarily.
template <typename T, pot::simd::SIMDType ST>
pot::coroutines::lazy_task<T>
l1_simd(pot::executor& exec, std::span<const T> a, std::span<const T> b) {
auto simd_abs_diff = [](auto va, auto vb) {
auto vd = va - vb;
return vd.abs();
};
auto scalar_abs_diff = [](T x, T y) { return std::abs(x - y); };
co_return co_await pot::algorithms::elementwise_reduce_simd<T, T, ST>(
exec, a, b, simd_abs_diff, scalar_abs_diff, std::plus<T>{}, T{0});
}Asynchronous dot product of two arrays.
// Regular version
auto res = co_await pot::algorithms::dot(exec, a, b);
// SIMD version
co_return co_await pot::algorithms::dot_simd<float, pot::simd::SIMDType::AVX>(exec, a, b);Asynchronous condition variable for coroutines. Does not block the thread — suspends the coroutine until signaled.
pot::coroutines::async_condition_variable cv;
pot::coroutines::task<void> waiter(int id) {
co_await cv;
std::cout << "Coroutine " << id << " resumed!" << std::endl;
}
pot::coroutines::task<void> example() {
auto task1 = waiter(1);
auto task2 = waiter(2);
std::this_thread::sleep_for(std::chrono::seconds(1));
cv.set(); // resumes all waiters
}| Method | Description |
|---|---|
async_condition_variable(bool set = false) |
Constructor. Optional initial state. |
operator co_await() |
Wait for signal. |
set() |
Set flag and resume all waiters. |
stop() |
Clear flag and discard waiters without resuming. |
is_set() |
Check if flag is set. |
reset() |
Clear flag. Waiters are not removed. |
Asynchronous barrier for coroutines. All coroutines suspend until set() has been called the required number of times.
pot::coroutines::async_barrier barrier(3); // wait for 3 set() calls
pot::coroutines::task<void> worker(int id) {
co_await barrier; // waits until all 3 workers call set()
std::cout << "Worker " << id << " passed the barrier" << std::endl;
}
// Caller:
co_await worker(1);
co_await worker(2);
co_await worker(3);
// All three complete → barrier lets them throughAsynchronous lock for coroutines. Does not block the thread — suspends the coroutine until the lock is acquired.
pot::sync::async_lock lock;
pot::executors::thread_pool_executor exec(4);
pot::coroutines::task<void> critical_section() {
auto guard = co_await lock.lock(&exec);
// guard — scoped_lock_guard, automatically calls unlock on scope exit
do_shared_work();
// unlock happens automatically
}Thread-safe wrapper around an object. Accessing via -> or * automatically acquires a mutex.
pot::sync::sync_object<std::vector<int>> safe_vec(std::vector<int>{1, 2, 3});
{
auto locked = safe_vec.scoped(); // std::scoped_lock
locked->push_back(4); // thread-safe
}Returns an awaitable that resumes the current coroutine on a given executor.
using pot::coroutines::resume_on;
pot::coroutines::task<void> do_work(pot::executor& cpu1, pot::executor& cpu2) {
co_await resume_on(cpu1);
co_await heavy_compute(); // on CPU1
co_await resume_on(cpu2);
update(); // on CPU2
}The library provides two SIMD classes: simd_forced (uses hardware intrinsics) and simd_auto (scalar fallback).
Forced SIMD execution for type T and instruction set ST.
pot::simd::simd_forced<float, pot::simd::SIMDType::AVX> a(1.0f);
pot::simd::simd_forced<float, pot::simd::SIMDType::AVX> b(2.0f);
auto c = a + b; // AVX addition
auto d = a * b; // AVX multiplication
auto e = a.abs(); // AVX absolute valueSupported T: int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, float, double.
Supported SIMDType:
SIMDType |
Register size | float count |
double count |
|---|---|---|---|
SSE |
128-bit | 4 | 2 |
AVX |
256-bit | 8 | 4 |
AVX512 |
512-bit | 16 | 8 |
Operations:
Arithmetic: +, -, *, /, %, unary -, +, ++, --, +=, -=, *=, /=, %=
Bitwise: &, |, ^, ~, <<, >>
Comparison: ==, !=, <, <=, >, >= (return bool)
Math: abs, sqrt, sqr, sum, prod, exp, log, log2, log10, sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh, ceil, floor, trunc, round, min, max
Scalar implementation with the same interface. Useful as a fallback when SIMD instructions are unavailable, or for portability.
pot::simd::simd_auto<float, 8> a(1.0f);
pot::simd::simd_auto<float, 8> b(2.0f);
auto c = a + b; // scalar additionMulti-producer single-consumer queue backed by a ring buffer.
pot::algorithms::lfqueue<int> queue;
// Producer:
queue.push(42);
// Consumer:
auto val = queue.pop();
if (val) {
std::cout << "Received: " << *val << std::endl;
}Multi-producer multi-consumer queue with tunable performance parameters.
orbit::mpmc_queue<int, 1024, true, true> queue; // MINIMISE_LATENCY=true, NONBLOCKING=true
queue.push(42);
auto val = queue.pop(); // blocking pop
bool ok = queue.try_pop(val); // non-blocking popTemplate parameters:
| Parameter | Default | Description |
|---|---|---|
SIZE |
— | Buffer size (must be a power of two). |
MINIMISE_LATENCY |
true |
Optimize for latency (false — throughput). |
NONBLOCKING |
true |
true — lock-free, false — removes CAS for lower latency. |
PAUSE_SHORT |
3 |
Spin-loop pause count. |
PAUSE_LONG |
40 |
Pause in throughput mode. |
Measure function execution time.
// Single measurement
auto duration = pot::utils::time_it<std::chrono::milliseconds>([] {
do_work();
});
// Average over N runs
auto avg = pot::utils::time_it<std::chrono::microseconds>(100, [] {
cleanup();
}, [] {
do_work();
});One-shot callable with SBO optimization (64 bytes).
pot::utils::unique_function_once f([] { std::cout << "called\n"; });
f(); // executes and is destroyed
// f(); // UB — calling twicestd::function replacement with PMR allocator support and SBO.
pot::utils::function<int(int, int)> add = [](int a, int b) { return a + b; };
int result = add(2, 3); // 5
// With custom allocator
pot::utils::function<void()> f(std::allocator_arg, my_resource, [] { ... });Cache line alignment constant.
constexpr std::size_t alignment = pot::cache_line_alignment; // typically 64Thread utilities: names, identifiers, priorities.
pot::this_thread::set_name("Worker");
auto name = pot::this_thread::name();
auto sys_id = pot::this_thread::system_id();
auto local_id = pot::this_thread::local_id();
pot::this_thread::set_params(SCHED_FIFO, 10); // scheduling policy and priorityCompile-time platform and compiler detection.
constexpr auto os = pot::platform::current_OS; // Linux, Windows, MacOS, ...
constexpr auto compiler = pot::platform::current_сompiler; // GCC, Clang, MSVC, ...Atomic progress tracker for parallel tasks.
pot::coroutines::details::progress p;
p.set_progress_range(0, 100);
p.set_progress_value(50);
p.set_progress_value_and_text(75, "Almost done...");
bool done = p.is_finished();