|
ThreadSchedule 3.0.0
Modern C++ thread management library
|
All standard APIs are available from:
Focused consumers can include a single self-contained contract instead:
| Facility | Header |
|---|---|
| Scheduling values | <threadschedule/scheduling.hpp>, <threadschedule/nice_value.hpp>, <threadschedule/realtime_priority.hpp> |
| Affinity and configuration | <threadschedule/cpu_id.hpp>, <threadschedule/thread_affinity.hpp>, <threadschedule/thread_config.hpp> |
| Owning and non-owning threads | <threadschedule/thread.hpp>, <threadschedule/thread_view.hpp> |
| C++20 joining thread | <threadschedule/jthread.hpp> |
| Calling-thread controls | <threadschedule/this_thread.hpp> |
| Registry | <threadschedule/thread_registry.hpp> |
| General pool | <threadschedule/thread_pool.hpp> |
| Scheduled work | <threadschedule/scheduled_task.hpp>, <threadschedule/scheduled_pool.hpp> |
| Runtime mode | <threadschedule/runtime.hpp> |
Every header above is tested in a fresh translation unit without a preceding umbrella include. core.hpp is the focused core umbrella and threadschedule.hpp is the recommended complete core include.
The core public surface is C++17. When the standard library exposes std::jthread, C++20 consumers additionally get threadschedule::jthread. This is the only language-standard-dependent core type.
The lowercase classes and configuration objects are independent v3 types, not aliases or public subclasses of the former PascalCase API. The standard-thread adapter lives under threadschedule::detail; specialized implementation types are supported only through the explicitly named advanced surface.
| Need | Type |
|---|---|
| One owning thread | thread |
| Cooperative cancellation under C++20 | jthread |
| Configure the calling thread | this_thread |
| General-purpose task execution | thread_pool |
| Delayed or periodic execution | scheduled_pool |
| Process thread discovery and control | thread_registry |
result<T> is an alias for expected<T, std::error_code>. Configuration, submission, and shutdown operations use this result type. Core objects are directly constructible; construction can throw like the corresponding standard library operation. The static create(...) factories remain available as an optional non-throwing construction path. Explicitly named *_or_throw helpers are available where a throwing operation is otherwise useful.
When the standard library provides C++23 std::expected, a threadschedule::expected<T, E> implicitly converts to the matching std::expected<T, E>. Converting an lvalue copies its active value or error; converting an rvalue moves it, including move-only payloads. Copy and move operations participate only when the stored types support them. State-changing assignment preserves the previous alternative if construction of the new one throws, expected<void, E> does not construct an inactive E, and transform(...) supports callables returning either a value or void.
An accepted task returns a standard future. Exceptions thrown by the task are stored in the future and rethrown by get(). A callback installed with thread_pool_config::set_error_callback(...) can observe the same exception as a task_error without consuming it. Fire-and-forget tasks submitted through post() have no future, so configure an error callback when their exceptions must be observed.
| Operation | Failure channel |
|---|---|
| Direct construction | Exception |
create(...) | result<T> |
| Configuration, submission, waiting, shutdown | result<T> |
Accepted submit(...) task | std::future |
Accepted post(...) task | Configured error callback |
Explicit *_or_throw helper | Exception |
thread owns a std::thread and joins it on destruction. Destruction and move assignment can therefore block until the currently owned thread exits. join, detach, and configure return result<void>; join_or_throw and detach_or_throw are the explicit throwing forms. Joining or detaching a non-joinable thread returns std::errc::invalid_argument. thread_view configures an existing std::thread or threadschedule::thread without taking ownership. Under C++20 it also accepts std::jthread and threadschedule::jthread.
For error-returning construction, pass thread_config to create(...) to apply a name, portable scheduling priority, and CPU affinity before the thread runs:
thread_affinity contains logical CPU indices. The portable scheduling factories include background, normal, interactive, and low_latency. The operating system can reject a name, scheduling request, or CPU mask, for example because a CPU is unavailable or the process lacks permission. Affinity changes succeed only when readback exactly matches the requested mask; a partially applied mask is rolled back when the platform permits it. create(...) reports initial-configuration failures as an error value; the direct constructor reports them like std::thread construction. If initial configuration fails, the callable is not started. Configuration operations preserve the specific error from the first failed name, scheduling, or affinity step.
Configuration objects use matching set_* and get_* names. A thread_config patch exposes get_name, get_scheduling, and get_affinity; pool configs expose get_worker_count, get_registration, get_worker_config, get_shutdown_policy, and get_error_callback (plus get_scheduler_config for scheduled pools).
The this_thread namespace applies the same portable settings to the calling thread, including threads created by another library:
this_thread provides configure, set_priority, set_nice, get_priority, get_nice, set_name, get_name, set_affinity, and get_affinity. All operations return result<T> and use the same validation and exact affinity readback as thread.
Under C++20, jthread mirrors std::jthread construction and cancellation:
It also accepts thread_config as its first constructor argument. There is no fallback jthread type in C++17.
Pool destruction normally joins the worker set according to the configured shutdown policy. If the last owner is released by one of the pool's own tasks, cleanup is transferred to a separate reaper thread so the current worker is not asked to join itself.
submit returns result<std::future<T>>; post returns result<void>. Like std::thread, pool submission stores decayed copies of the callable and arguments and invokes those stored objects as rvalues. The future's value type is determined from that stored invocation; use std::ref when reference semantics are required. Destruction uses the configured shutdown policy. drain completes accepted work, while drop_pending discards work that has not started. Calling shutdown() uses that same configured policy; the shutdown(shutdown_policy) overload explicitly overrides it for that call. After a move, the source pool has size zero. Submission, waiting, and worker configuration return operation_canceled; shutdown remains an idempotent success. A shutdown pool retains its configured size() even though its workers have already been joined.
Calling wait() or shutdown() from one of the same pool's worker tasks is rejected with std::errc::resource_deadlock_would_occur. Releasing the last owner from one of its own tasks is safe: destruction transfers backend cleanup to a reaper thread after the current task returns.
Periodic intervals must be positive. Periodic tasks use fixed-rate scheduling: each next deadline is based on the preceding deadline, not task completion. An occurrence never overlaps with itself; deadlines missed while it is still running are skipped instead of building a worker-blocking backlog. Cancellation is cooperative and does not interrupt a running task. Scheduling after shutdown returns std::errc::operation_canceled. Delays, initial delays, and periodic deadlines that cannot be represented by steady_clock return std::errc::value_too_large instead of wrapping into an earlier deadline.
scheduled_pool_config supports the same worker registration, worker configuration, shutdown policy, and task-error callback as thread_pool_config, plus an independent scheduler thread configuration. Shutdown stops accepting and dispatching scheduled entries; the selected policy controls work already queued in the worker pool. Calling shutdown from one of its worker tasks or from scheduler-thread cleanup is rejected with std::errc::resource_deadlock_would_occur before shutdown state changes. Releasing the last owner from a scheduled worker is safe and uses the same reaper cleanup as thread_pool. Under drop_pending, handles for scheduled tasks already dispatched but discarded from the worker queue report is_cancelled() == true.
registered_thread is a lowercase value snapshot without native control-block ownership. Its id is the OS-backed thread_id used by registry operations; its std_id is the separate std::thread::id. global_registry() returns the active process registry. A scoped global_registry_binding installs an application-owned registry, keeps its backend alive, and restores the previous registry when the binding is destroyed. Bindings must be destroyed in reverse installation order. Installing, moving, or destroying a binding, and move-assigning a bound registry, must not run concurrently with operations through global_registry(); install bindings during application startup and destroy them only after registry users have stopped. An auto_register_current_thread guard likewise retains the owning registry backend, so it can safely outlive the thread_registry facade passed to its constructor. Move-assigning a bound registry retargets its binding to the replacement backend. This also applies while an outer binding is temporarily hidden by a nested binding; the replacement remains owned until that binding is destroyed. Header-only builds have one instance per linked image; the optional runtime supplies one instance to compatible DSOs that link it.
Entries added by register_current_thread retain a native control block, so their thread_id can be passed to thread_registry::configure while the registered thread remains alive. After a move, the source registry reads as empty and mutating operations return operation_canceled. Assigning a new registry makes it usable again. Constructing auto_register_current_thread with a moved-from registry throws std::system_error with operation_canceled, because a constructor cannot return a result.
The portable factories are background, normal, interactive, low_latency, priority, nice, realtime_fifo, and realtime_rr.
schedule::priority(priority_level) provides lowest, low, normal, high, and highest. Their Linux nice values are respectively 19, 5, 0, -5, and -20. schedule::nice(nice_value{value}) exposes the full -20 through 19 scale. Portable realtime factories take realtime_priority{value} in the range 1 through 99. Invalid direct construction throws std::invalid_argument; the parallel create(...) factories return result<T>.
On Windows, normal priorities map to IDLE, BELOW_NORMAL, NORMAL, ABOVE_NORMAL, or HIGHEST. Exact nice values use the same safe mapping and never select TIME_CRITICAL. Portable realtime requests map only to ABOVE_NORMAL or HIGHEST. Code requiring other native behavior can call the platform API through advanced::native_handle(...). MinGW-w64 uses the same Win32 behavior through its pthread-to-HANDLE adapter.
thread, C++20 jthread, thread_view, and this_thread provide set_priority, set_nice, get_priority, get_nice, and error-preserving get_affinity operations. Linux readback reports the effective portable value: SCHED_IDLE maps to lowest/nice 19, while realtime and other policies without nice semantics report operation_not_supported. A Linux thread_view over an external std::thread has no portable identity for nice control, so nice operations also report operation_not_supported. The same limitation applies to thread and jthread objects that adopt existing standard threads. Native identity-based control is available only under advanced. Registry-managed threads expose matching operations by thread_id, and pool workers use the same settings through thread_config.
Increasing priority by lowering the numeric nice value usually requires privileges on Linux, including restoring a positive nice value to zero. Leaving SCHED_IDLE after schedule::background() can require the same privileges. Applying realtime policies can likewise fail with permission_denied or operation_not_permitted. Platform-native policy manipulation is done directly through the operating-system API and an advanced native handle.