All standard APIs are available from:
#include <threadschedule/threadschedule.hpp>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.
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 |
#include <future>
std::promise<void> release;
auto ready_to_finish = release.get_future().share();
threadschedule::thread worker([ready_to_finish] { ready_to_finish.wait(); });
if (auto result = worker.set_name("worker"); !result)
{
report(result.error());
}
if (auto result = worker.set_affinity(
threadschedule::thread_affinity({ threadschedule::cpu_id{0} })); !result)
{
report(result.error());
}
threadschedule::thread_config config;
config.set_scheduling(threadschedule::schedule::background());
if (auto result = worker.configure(config); !result)
{
report(result.error());
}
release.set_value();
if (auto result = worker.join(); !result)
{
report(result.error());
}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:
threadschedule::thread_config config;
config.set_name("metrics")
.set_scheduling(threadschedule::schedule::background())
.set_affinity(threadschedule::thread_affinity(
{ threadschedule::cpu_id{0}, threadschedule::cpu_id{1} }));
if (auto worker = threadschedule::thread::create(config, [] {
// Collect metrics on the configured thread.
});
!worker)
{
report(worker.error());
}
else if (auto result = worker->join(); !result)
{
report(result.error());
}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:
auto allowed = threadschedule::this_thread::get_affinity();
if (!allowed)
{
report(allowed.error());
}
else
{
threadschedule::thread_affinity pinned({ allowed->cpus().front() });
if (auto result = threadschedule::this_thread::set_affinity(pinned);
!result)
report(result.error());
}
if (auto result = threadschedule::this_thread::set_priority(
threadschedule::priority_level::low);
!result)
report(result.error());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:
#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L
threadschedule::jthread worker([](std::stop_token stop) {
while (!stop.stop_requested())
do_work();
});
worker.request_stop();
#endifIt also accepts thread_config as its first constructor argument. There is no
fallback jthread type in C++17.
threadschedule::thread_pool_config config;
config.set_worker_count(threadschedule::worker_count{8})
.set_registration(threadschedule::worker_registration::global_registry)
.set_shutdown_policy(threadschedule::shutdown_policy::drain);
threadschedule::thread_pool pool(std::move(config));
auto calculated = pool.submit([] { return calculate(); });
if (!calculated)
report(calculated.error());
else
use(calculated->get());
if (auto posted = pool.post([] { publish_metrics(); }); !posted)
report(posted.error());
if (auto waited = pool.wait(); !waited)
report(waited.error());submit returns result<std::future<T>>; post returns result<void>.
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.
Calling wait() or shutdown() from one of the same pool's worker tasks is
rejected with std::errc::resource_deadlock_would_occur. Destroying a pool
from one of its own tasks is unsupported because the task is still using that
pool; arrange destruction from an external owner after the task returns.
threadschedule::scheduled_pool_config config;
threadschedule::thread_config workers;
workers.set_name("scheduled-worker");
threadschedule::thread_config scheduler_thread;
scheduler_thread.set_name("scheduler");
config.set_worker_count(threadschedule::worker_count{2})
.set_registration(threadschedule::worker_registration::global_registry)
.set_worker_config(std::move(workers))
.set_scheduler_config(std::move(scheduler_thread))
.set_shutdown_policy(threadschedule::shutdown_policy::drain)
.set_error_callback([](threadschedule::task_error const& error) {
report(error.what());
});
threadschedule::scheduled_pool scheduler(std::move(config));
auto once = scheduler.schedule_after(250ms, [] { refresh(); });
auto periodic = scheduler.schedule_periodic(1s, [] { sample(); });
auto delayed = scheduler.schedule_periodic_after(
5s, 1s, [] { sample_after_warmup(); });
if (!once)
report(once.error());
if (!delayed)
report(delayed.error());
if (!periodic)
report(periodic.error());
else
periodic->cancel();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.
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.
threadschedule::thread_registry registry;
if (auto registered = registry.register_current_thread("main", "application");
!registered)
report(registered.error());
auto entries = registry.snapshot();
if (!entries)
report(entries.error());
else
for (auto const& entry : *entries)
inspect(entry.name, entry.component);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.
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. 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 with a negative nice value usually requires privileges on
Linux. 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.