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>.
- 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.
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/prefixfind_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=missingThe recipe is tested in CI. Its standard shared=True option packages the
optional ThreadSchedule::Runtime; header-only mode remains the default.
#include <threadschedule/threadschedule.hpp>
#include <iostream>
int main()
{
threadschedule::thread_pool pool(2);
auto answer = pool.submit([] { return 42; });
if (!answer) {
std::cerr << answer.error().message() << '\n';
return 1;
}
std::cout << answer->get() << '\n';
}The complete
getting-started project includes its
own CMakeLists.txt and is tested against a freshly installed package.
| 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 |
| 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:
#include <threadschedule/thread.hpp>
#include <threadschedule/thread_config.hpp>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.
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 on_task_error |
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; configure on_task_error 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.
Direct construction is the ordinary path:
threadschedule::thread worker([] { do_work(); });
if (auto joined = worker.join(); !joined)
report(joined.error());Use create(...) when initial configuration failures should be returned as an
error value:
threadschedule::thread_config config;
config.name = "metrics";
config.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());
}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:
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 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();
});
worker.request_stop();
#endifSee the compile-tested jthread example.
threadschedule::thread_pool_config config;
config.worker_count = 4;
config.workers.name = "worker";
config.on_task_error = [](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());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.
Portable intent factories cover ordinary use:
auto background = threadschedule::schedule::background();
auto interactive = threadschedule::schedule::interactive();
auto low_latency = threadschedule::schedule::low_latency();
auto lower_priority = threadschedule::schedule::priority(
threadschedule::priority_level::low);
auto exact_nice = threadschedule::schedule::nice(10);
auto realtime = threadschedule::schedule::realtime_fifo(80);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.
#include <threadschedule/advanced.hpp>
threadschedule::advanced::work_stealing_pool pool(8);
auto future = pool.submit(expensive_work);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.
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.
- Online API reference
- API overview
- Advanced APIs
- CMake reference
- Compatibility and ABI
- Migrating from 2.x
- Changelog
ThreadSchedule is available under the MIT License.