ThreadSchedule 3.0.0
Modern C++ thread management library
Loading...
Searching...
No Matches
ThreadSchedule

Tests Runtime Tests Documentation [License](LICENSE)

ThreadSchedule is a C++17 library for creating, configuring, scheduling, and observing threads on Linux and Windows. It is header-only by default. C++20 consumers additionally get threadschedule::jthread when the standard library provides std::jthread.

The v3 core deliberately stays small and uses lowercase, standard-style names. Operations whose normal failure mode should not require exceptions return threadschedule::expected<T, std::error_code>.

Requirements

  • CMake 3.14 or newer
  • C++17 or newer
  • Linux with GCC/libstdc++, or Windows with MinGW-w64/GCC or MSVC

The tested compiler versions are the compatibility contract. See Compatibility for the current matrix.

GCC 14 ThreadSanitizer limitation

GCC 14's ThreadSanitizer can incorrectly report unlock of an unlocked mutex (or by a wrong thread) when a pool uses shutdown_for(...). libstdc++ acquires the timed mutex through pthread_mutex_clocklock, which GCC 14's TSan does not fully intercept, but it does observe the later unlock. This is a sanitizer false positive rather than an unmatched unlock in ThreadSchedule. The sanitizer CI therefore uses GCC 16, where the same tests pass cleanly.

Install

The recommended source integration uses CMake FetchContent:

include(FetchContent)
FetchContent_Declare(
ThreadSchedule
GIT_REPOSITORY https://github.com/Katze719/ThreadSchedule.git
GIT_TAG v3.0.0
)
FetchContent_MakeAvailable(ThreadSchedule)
target_link_libraries(my_app PRIVATE ThreadSchedule::ThreadSchedule)

An existing checkout can be added directly:

add_subdirectory(path/to/ThreadSchedule)
target_link_libraries(my_app PRIVATE ThreadSchedule::ThreadSchedule)

To install and consume the CMake package:

cmake -S . -B build -DTHREADSCHEDULE_INSTALL=ON
cmake --build build
cmake --install build --prefix /your/prefix
find_package(ThreadSchedule 3 CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE ThreadSchedule::ThreadSchedule)

Conan 2 consumers can build a local package directly from the release source:

conan profile detect
conan create . --build=missing

Windows Vista compatibility mode is available when older platform targeting is required. It reduces Windows feature usage to avoid Win7+ only paths. This mode is currently not tested on real Vista hardware and may be unstable. Validation is limited because no active Vista test machine is available.

cmake -S . -B build -DTHREADSCHEDULE_WINDOWS_VISTA_COMPAT=ON
conan create . -o '&:windows_vista_compat=True' --build=missing

The recipe is tested in CI. Its standard shared=True option packages the optional ThreadSchedule::Runtime; header-only mode remains the default.

Start in five minutes

#include <iostream>
int main()
{
auto answer = pool.submit([] { return 42; });
if (!answer) {
std::cerr << answer.error().message() << '\n';
return 1;
}
std::cout << answer->get() << '\n';
}
Fixed-size thread pool for queued asynchronous work.
auto submit(F &&function, Args &&... args) -> result< std::future< detail::bind_result_t< F, Args... > > >
Submit work and receive a future for its result.
Value type for pool worker count.
ThreadSchedule's C++17 core surface and optional C++20 jthread.

The complete getting-started project includes its own CMakeLists.txt and is tested against a freshly installed package.

Choose the right type

Need Start with
Own one thread thread
Own one cooperatively cancellable C++20 thread jthread
Configure the calling thread this_thread
Submit general-purpose work thread_pool
Run delayed or periodic work scheduled_pool
Discover and control registered threads thread_registry
Find unregistered Linux threads by OS name advanced::thread_by_name_view
Select a specialized pool or native control advanced::*

Include <threadschedule/threadschedule.hpp> for the complete core. Include <threadschedule/advanced.hpp> only when the workload requires native or specialized choices.

For small consumers, each core contract is independently includable. For example, a single managed thread needs only:

Owning std::thread wrapper with portable configuration helpers.
Portable thread startup and runtime configuration.

Pools can use <threadschedule/thread_pool.hpp> or <threadschedule/scheduled_pool.hpp> directly; registry-only code can use <threadschedule/thread_registry.hpp>. The focused headers avoid making an application opt into unrelated APIs, while threadschedule.hpp remains the convenient complete core umbrella.

Results, exceptions, and lifetime

ThreadSchedule keeps failure channels explicit:

Operation Failure channel
Direct construction May throw std::system_error, like standard types
create(...) Returns expected<T, std::error_code>
Configuration and shutdown Return expected<void, std::error_code>
thread_pool::submit(...) Submission error in expected; task exception in the future
thread_pool::post(...) Submission error in expected; task exception via the configured error callback
Explicit *_or_throw operation Throws std::system_error on failure

Always inspect an expected before dereferencing it. A task submitted with post() has no future; call set_error_callback(...) on the pool config if its exceptions must be observed.

threadschedule::thread owns a std::thread but deliberately joins a joinable thread on destruction. Destruction and move assignment can therefore block. Call join(), detach(), or release() explicitly when that timing matters.

Threads and configuration

Direct construction is the ordinary path:

threadschedule::thread worker([] { do_work(); });
if (auto joined = worker.join(); !joined)
report(joined.error());
Owning thread wrapper with result-based lifecycle/configuration API.
Definition thread.hpp:29

Use create(...) when initial configuration failures should be returned as an error value:

config.set_name("metrics").set_scheduling(threadschedule::schedule::background());
auto worker = threadschedule::thread::create(config, [] {
collect_metrics();
});
if (!worker) {
report(worker.error());
} else if (auto joined = worker->join(); !joined) {
report(joined.error());
}
Portable thread configuration bundle.
auto set_name(std::string name) -> thread_config &
Set thread name.
static auto create(F &&function, Args &&... args) -> std::enable_if_t<!std::is_same_v< std::decay_t< F >, thread_config >, result< thread > >
Create thread without throwing.
Definition thread.hpp:76
constexpr auto background() noexcept -> scheduling_config
Request background scheduling.

Affinity uses logical CPU indices and is intentionally absent from this first configured example: containers and restricted CPU sets may not make CPU 0 available. Query the deployment environment before pinning a thread.

Code running inside any thread can configure itself without wrapping or registering the thread first:

if (!allowed) {
report(allowed.error());
} else {
threadschedule::thread_affinity pinned({ allowed->cpus().front() });
if (auto result = threadschedule::this_thread::set_affinity(pinned);
report(result.error());
}
report(result.error());
auto get_affinity() -> result< thread_affinity >
Query CPU affinity of the calling thread.
auto set_priority(priority_level level) -> result< void >
Set portable priority preset for the calling thread.
auto set_affinity(thread_affinity const &affinity) -> result< void >
Set CPU affinity for the calling thread.
expected< T, std::error_code > result
Standard result type used by public APIs.
Definition result.hpp:22

this_thread also provides configure, set_nice, get_priority, set_name, and get_name. Affinity readback reports the logical CPU indices the process is actually allowed to use, which is safer than assuming CPU 0 is available.

Under C++20, jthread mirrors standard callable forwarding and stop-token injection:

#if defined(__cpp_lib_jthread) && __cpp_lib_jthread >= 201911L
threadschedule::jthread worker([](std::stop_token stop) {
while (!stop.stop_requested())
do_work();
});
(void)worker.request_stop();
#endif

See the compile-tested jthread example.

Thread pools

workers.set_name("worker");
.set_worker_config(std::move(workers))
.set_error_callback([](threadschedule::task_error const& error) {
log(error.what());
});
threadschedule::thread_pool pool(std::move(config));
auto answer = pool.submit([] { return calculate(); });
if (!answer)
report(answer.error());
else
use(answer->get());
Builder-style configuration for thread_pool.
auto set_worker_count(worker_count value) noexcept -> thread_pool_config &
Set number of worker threads.
Captured information about a task failure.
auto what() const -> std::string
Return a printable error message if exception derives from std::exception.

Task exceptions from submit() remain attached to the returned future and are rethrown by get(). Direct pool construction can throw when worker creation or configuration fails; thread_pool::create(...) offers the error-value path.

Scheduling

Portable intent factories cover ordinary use:

auto lower_priority = threadschedule::schedule::priority(
Strongly-typed nice value in the POSIX range $[-20, 19]$.
Strongly-typed realtime priority in the range $[1, 99]$.
constexpr auto nice(nice_value value) noexcept -> scheduling_config
Request scheduling via explicit nice value.
constexpr auto realtime_fifo(realtime_priority priority) noexcept -> scheduling_config
Request realtime FIFO scheduling.
constexpr auto interactive() noexcept -> scheduling_config
Request interactive scheduling.
constexpr auto priority(priority_level level) noexcept -> scheduling_config
Request scheduling via portable priority level presets.
constexpr auto low_latency() noexcept -> scheduling_config
Request low-latency scheduling.

The five priority_level values are the simplest cross-platform choice. Negative nice values and realtime policies normally require elevated privileges on Linux. Native scheduling remains available through threadschedule::advanced.

Advanced usage

auto future = pool.submit(expensive_work);
if (!future)
report(future.error());
else
use(future->get());
Optional ThreadSchedule facilities beyond the portable core API.

On Linux, an unregistered process thread can also be found by its exact kernel-visible name. A singular lookup rejects duplicate names; use find_all() when duplicates are intentional:

if (!worker)
report(worker.error());
else if (auto lowered = worker->set_priority(
!lowered)
report(lowered.error());
static auto create(std::string_view name) -> result< thread_by_name_view >
Find exactly one named process thread without throwing.

The view remembers the Linux TID and its start-time generation, so exited or recycled targets report no_such_process. This native lookup cannot fully close the race between the last identity check and a TID-based syscall; use thread_registry when target lifetime must be coupled to control operations.

The advanced namespace is public and follows semantic versioning. See Advanced APIs for native controls, profiles, topology, future combinators, task groups, chaos testing, and lower-level error handling.

Optional shared registry runtime

Header-only mode owns one registry per linked image. Applications that need one registry shared by an executable and compatible DSOs can link the optional C++ runtime:

set(THREADSCHEDULE_RUNTIME ON)
add_subdirectory(ThreadSchedule)
target_link_libraries(my_app PRIVATE ThreadSchedule::Runtime)

This is a same-toolchain C++ ABI, not a portable plugin ABI. Do not mix GCC, MinGW, and MSVC artifacts.

Documentation

License

ThreadSchedule is available under the [MIT License](LICENSE).