From c1b2e63c98c2301302bf0523097a8490e3e5c5e8 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Thu, 28 May 2026 19:29:15 +0200 Subject: [PATCH 1/5] feat: release v2.2.0 with new features and internal improvements - Introduced `ThreadInfo` for managing thread IDs and configurations. - Enhanced `ScheduledThreadPoolT` and `ChaosController` for better thread management. - Added methods for configuring thread names, priorities, and scheduling policies. - Updated changelog and version to reflect new release. --- CHANGELOG.md | 27 +++ VERSION | 2 +- include/threadschedule/chaos.hpp | 35 +++- include/threadschedule/scheduled_pool.hpp | 33 +++- include/threadschedule/scheduler_policy.hpp | 177 ++++++++++++++++++++ include/threadschedule/thread_registry.hpp | 6 - include/threadschedule/thread_wrapper.hpp | 146 +++++++++------- include/threadschedule/threadschedule.hpp | 2 + tests/thread_config_test.cpp | 43 +++++ tests/thread_pool_v2_test.cpp | 33 ++++ 10 files changed, 434 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e4876..ed961a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## v2.2.0 + +> **No intended API/ABI breaking changes.** This release extends thread-control +> coverage to library-owned background threads and expands `ThreadInfo` into a +> lightweight per-thread control handle. + +### New Features + +- **`ThreadInfo` now supports bound thread IDs** -- it can be default-constructed + for the current thread or explicitly constructed from a `Tid`, then used to + `set_name`, `get_name`, `set_priority`, `set_scheduling_policy`, + `set_affinity`, `get_affinity`, `get_policy`, and `get_priority`. + The existing static convenience methods remain available. (`thread_wrapper.hpp`) + +- **Library-owned background threads are now configurable** -- `ScheduledThreadPoolT` + exposes `scheduler_thread_info()` and `configure_scheduler_thread(...)`, and + `ChaosController` exposes `thread_info()` and `configure_thread(...)`, so the + scheduler/control threads are no longer anonymous internal `std::thread`s. + (`scheduled_pool.hpp`, `chaos.hpp`) + +### Internal Improvements + +- **Dedicated background threads now use the same wrapper/control path as worker + threads** -- scheduler and chaos threads are created as `ThreadWrapper`s and + receive stable default names, keeping thread-control behavior consistent + across the library. (`scheduled_pool.hpp`, `chaos.hpp`) + ## v2.1.0 > **No API/ABI breaking changes.** All modifications are bug fixes (aligning diff --git a/VERSION b/VERSION index 227cea2..ccbccc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 +2.2.0 diff --git a/include/threadschedule/chaos.hpp b/include/threadschedule/chaos.hpp index dceee1d..cf64300 100644 --- a/include/threadschedule/chaos.hpp +++ b/include/threadschedule/chaos.hpp @@ -16,6 +16,7 @@ #include "topology.hpp" #include #include +#include #include #include @@ -51,7 +52,7 @@ struct ChaosConfig * @brief RAII controller that periodically perturbs scheduling attributes * of registered threads for chaos/fuzz testing. * - * On construction, `ChaosController` spawns a background `std::thread` + * On construction, `ChaosController` spawns a background control thread * that wakes every `ChaosConfig::interval` and applies perturbations * (affinity shuffling, priority jitter) to threads in the global * `registry()` that match the user-supplied predicate. @@ -86,9 +87,18 @@ class ChaosController { public: template - ChaosController(ChaosConfig cfg, Predicate pred) - : config_(cfg), stop_(false), worker_([this, pred]() { run_loop(pred); }) + ChaosController(ChaosConfig cfg, Predicate pred) : config_(cfg), stop_(false) { + std::promise worker_started; + auto worker_ready = worker_started.get_future(); + + worker_ = ThreadWrapper([this, pred, started = std::move(worker_started)]() mutable { + started.set_value(ThreadInfo::get_thread_id()); + run_loop(pred); + }); + + worker_tid_ = worker_ready.get(); + (void)ThreadInfo(worker_tid_).set_name("ts_chaos_ctl"); } ~ChaosController() @@ -101,6 +111,22 @@ class ChaosController ChaosController(ChaosController const&) = delete; auto operator=(ChaosController const&) -> ChaosController& = delete; + [[nodiscard]] auto thread_info() const -> std::optional + { + if (!worker_.joinable() || worker_tid_ == Tid{}) + return std::nullopt; + return ThreadInfo(worker_tid_); + } + + auto configure_thread(std::string const& name, SchedulingPolicy policy = SchedulingPolicy::OTHER, + ThreadPriority priority = ThreadPriority::normal()) -> expected + { + auto info = thread_info(); + if (!info.has_value()) + return unexpected(std::make_error_code(std::errc::no_such_process)); + return detail::configure_thread(info.value(), name, policy, priority); + } + private: template void run_loop(Predicate pred) @@ -148,7 +174,8 @@ class ChaosController ChaosConfig config_; std::atomic stop_; - std::thread worker_; + ThreadWrapper worker_; + Tid worker_tid_{}; }; } // namespace threadschedule diff --git a/include/threadschedule/scheduled_pool.hpp b/include/threadschedule/scheduled_pool.hpp index 4001ca1..2f57f7f 100644 --- a/include/threadschedule/scheduled_pool.hpp +++ b/include/threadschedule/scheduled_pool.hpp @@ -9,6 +9,8 @@ #include "thread_pool.hpp" #include #include +#include +#include #include #include #include @@ -158,7 +160,16 @@ class ScheduledThreadPoolT explicit ScheduledThreadPoolT(size_t worker_threads = std::thread::hardware_concurrency()) : pool_(worker_threads), stop_(false), next_task_id_(1) { - scheduler_thread_ = std::thread(&ScheduledThreadPoolT::scheduler_loop, this); + std::promise scheduler_started; + auto scheduler_ready = scheduler_started.get_future(); + + scheduler_thread_ = ThreadWrapper([this, started = std::move(scheduler_started)]() mutable { + started.set_value(ThreadInfo::get_thread_id()); + scheduler_loop(); + }); + + scheduler_tid_ = scheduler_ready.get(); + (void)ThreadInfo(scheduler_tid_).set_name("ts_sched_pool"); } ScheduledThreadPoolT(ScheduledThreadPoolT const&) = delete; @@ -280,9 +291,27 @@ class ScheduledThreadPoolT return pool_.configure_threads(name_prefix, policy, priority); } + [[nodiscard]] auto scheduler_thread_info() const -> std::optional + { + if (!scheduler_thread_.joinable() || scheduler_tid_ == Tid{}) + return std::nullopt; + return ThreadInfo(scheduler_tid_); + } + + auto configure_scheduler_thread(std::string const& name, SchedulingPolicy policy = SchedulingPolicy::OTHER, + ThreadPriority priority = ThreadPriority::normal()) + -> expected + { + auto info = scheduler_thread_info(); + if (!info.has_value()) + return unexpected(std::make_error_code(std::errc::no_such_process)); + return detail::configure_thread(info.value(), name, policy, priority); + } + private: PoolType pool_; - std::thread scheduler_thread_; + ThreadWrapper scheduler_thread_; + Tid scheduler_tid_{}; mutable std::mutex mutex_; std::condition_variable condition_; diff --git a/include/threadschedule/scheduler_policy.hpp b/include/threadschedule/scheduler_policy.hpp index cb167d7..685ecec 100644 --- a/include/threadschedule/scheduler_policy.hpp +++ b/include/threadschedule/scheduler_policy.hpp @@ -8,6 +8,7 @@ #include "expected.hpp" #include #include +#include #include #include #include @@ -20,12 +21,19 @@ #include #include #include +#include #endif namespace threadschedule { // expected/result are provided by expected.hpp +#ifdef _WIN32 +using Tid = unsigned long; // DWORD thread id +#else +using Tid = pid_t; // Linux TID via gettid() +#endif + /** * @brief Enumeration of available thread scheduling policies. * @@ -632,6 +640,112 @@ inline auto read_affinity(HANDLE handle) -> std::optional return std::nullopt; } +inline auto read_priority(HANDLE handle) -> std::optional +{ + if (!handle) + return std::nullopt; + int const priority = GetThreadPriority(handle); + if (priority == THREAD_PRIORITY_ERROR_RETURN) + return std::nullopt; + return priority; +} + +inline auto read_scheduling_policy(HANDLE handle) -> std::optional +{ + if (!handle) + return std::nullopt; + return SchedulingPolicy::OTHER; +} + +inline auto apply_priority(Tid tid, ThreadPriority priority) -> expected +{ + HANDLE handle = OpenThread(THREAD_SET_INFORMATION, FALSE, tid); + if (!handle) + return unexpected(std::make_error_code(std::errc::no_such_process)); + + auto result = apply_priority(handle, priority); + CloseHandle(handle); + return result; +} + +inline auto apply_scheduling_policy(Tid tid, SchedulingPolicy policy, ThreadPriority priority) + -> expected +{ + HANDLE handle = OpenThread(THREAD_SET_INFORMATION, FALSE, tid); + if (!handle) + return unexpected(std::make_error_code(std::errc::no_such_process)); + + auto result = apply_scheduling_policy(handle, policy, priority); + CloseHandle(handle); + return result; +} + +inline auto apply_affinity(Tid tid, ThreadAffinity const& affinity) -> expected +{ + HANDLE handle = OpenThread(THREAD_SET_INFORMATION, FALSE, tid); + if (!handle) + return unexpected(std::make_error_code(std::errc::no_such_process)); + + auto result = apply_affinity(handle, affinity); + CloseHandle(handle); + return result; +} + +inline auto apply_name(Tid tid, std::string const& name) -> expected +{ + HANDLE handle = OpenThread(THREAD_SET_LIMITED_INFORMATION, FALSE, tid); + if (!handle) + return unexpected(std::make_error_code(std::errc::no_such_process)); + + auto result = apply_name(handle, name); + CloseHandle(handle); + return result; +} + +inline auto read_name(Tid tid) -> std::optional +{ + HANDLE handle = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tid); + if (!handle) + return std::nullopt; + + auto result = read_name(handle); + CloseHandle(handle); + return result; +} + +inline auto read_affinity(Tid tid) -> std::optional +{ + HANDLE handle = OpenThread(THREAD_QUERY_INFORMATION, FALSE, tid); + if (!handle) + return std::nullopt; + + auto result = read_affinity(handle); + CloseHandle(handle); + return result; +} + +inline auto read_priority(Tid tid) -> std::optional +{ + HANDLE handle = OpenThread(THREAD_QUERY_INFORMATION, FALSE, tid); + if (!handle) + return std::nullopt; + + auto result = read_priority(handle); + CloseHandle(handle); + return result; +} + +inline auto read_scheduling_policy(Tid tid) -> std::optional +{ + HANDLE handle = OpenThread(THREAD_QUERY_INFORMATION, FALSE, tid); + if (!handle) + return std::nullopt; + + auto result = read_scheduling_policy(handle); + CloseHandle(handle); + return result; +} + #else // POSIX // --- shared implementation for pthread_t and pid_t scheduling --- @@ -725,6 +839,69 @@ inline auto apply_affinity(pid_t tid, ThreadAffinity const& affinity) -> expecte return unexpected(std::error_code(errno, std::generic_category())); } +inline auto apply_name(pid_t tid, std::string const& name) -> expected +{ + if (name.length() > 15) + return unexpected(std::make_error_code(std::errc::invalid_argument)); + + std::string const path = std::string("/proc/self/task/") + std::to_string(tid) + "/comm"; + std::ofstream out(path); + if (!out) + return unexpected(std::error_code(errno, std::generic_category())); + + out << name; + out.flush(); + if (!out) + return unexpected(std::error_code(errno, std::generic_category())); + return {}; +} + +inline auto read_name(pid_t tid) -> std::optional +{ + std::string const path = std::string("/proc/self/task/") + std::to_string(tid) + "/comm"; + std::ifstream in(path); + if (!in) + return std::nullopt; + + std::string current; + std::getline(in, current); + if (!current.empty() && current.back() == '\n') + current.pop_back(); + return current; +} + +inline auto read_affinity(pid_t tid) -> std::optional +{ + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + if (sched_getaffinity(tid, sizeof(cpu_set_t), &cpuset) != 0) + return std::nullopt; + + std::vector cpus; + for (int i = 0; i < CPU_SETSIZE; ++i) + { + if (CPU_ISSET(i, &cpuset)) + cpus.push_back(i); + } + return ThreadAffinity(cpus); +} + +inline auto read_priority(pid_t tid) -> std::optional +{ + sched_param param{}; + if (sched_getparam(tid, ¶m) == 0) + return param.sched_priority; + return std::nullopt; +} + +inline auto read_scheduling_policy(pid_t tid) -> std::optional +{ + int const policy = sched_getscheduler(tid); + if (policy == -1) + return std::nullopt; + return static_cast(policy); +} + #endif } // namespace detail diff --git a/include/threadschedule/thread_registry.hpp b/include/threadschedule/thread_registry.hpp index baf5d5b..99d4c26 100644 --- a/include/threadschedule/thread_registry.hpp +++ b/include/threadschedule/thread_registry.hpp @@ -42,12 +42,6 @@ namespace threadschedule #define THREADSCHEDULE_API #endif -#ifdef _WIN32 -using Tid = unsigned long; // DWORD thread id -#else -using Tid = pid_t; // Linux TID via gettid() -#endif - /** * @brief Snapshot of metadata for a single registered thread. * diff --git a/include/threadschedule/thread_wrapper.hpp b/include/threadschedule/thread_wrapper.hpp index 19a3176..700c019 100644 --- a/include/threadschedule/thread_wrapper.hpp +++ b/include/threadschedule/thread_wrapper.hpp @@ -110,6 +110,20 @@ class ThreadStorage ThreadType* external_thread_ = nullptr; // non-owning }; + +template +inline auto configure_thread(ThreadLike& thread, std::string const& name, SchedulingPolicy policy, + ThreadPriority priority) -> expected +{ + bool success = true; + if (!thread.set_name(name).has_value()) + success = false; + if (!thread.set_scheduling_policy(policy, priority).has_value()) + success = false; + if (success) + return {}; + return unexpected(std::make_error_code(std::errc::operation_not_permitted)); +} } // namespace detail /** @@ -671,7 +685,7 @@ class ThreadByNameView #ifdef _WIN32 using native_handle_type = void*; // unsupported placeholder #else - using native_handle_type = pid_t; // Linux TID + using native_handle_type = Tid; // Linux TID #endif explicit ThreadByNameView(const std::string& name) @@ -723,17 +737,7 @@ class ThreadByNameView #else if (!found()) return unexpected(std::make_error_code(std::errc::no_such_process)); - if (name.length() > 15) - return unexpected(std::make_error_code(std::errc::invalid_argument)); - std::string path = std::string("/proc/self/task/") + std::to_string(handle_) + "/comm"; - std::ofstream out(path); - if (!out) - return unexpected(std::error_code(errno, std::generic_category())); - out << name; - out.flush(); - if (!out) - return unexpected(std::error_code(errno, std::generic_category())); - return {}; + return detail::apply_name(handle_, name); #endif } @@ -744,15 +748,7 @@ class ThreadByNameView #else if (!found()) return std::nullopt; - std::string path = std::string("/proc/self/task/") + std::to_string(handle_) + "/comm"; - std::ifstream in(path); - if (!in) - return std::nullopt; - std::string current; - std::getline(in, current); - if (!current.empty() && current.back() == '\n') - current.pop_back(); - return current; + return detail::read_name(handle_); #endif } @@ -804,30 +800,89 @@ class ThreadByNameView }; /** - * @brief Static utility class providing hardware and scheduling introspection. + * @brief Lightweight handle for querying and controlling a specific OS thread. * - * All methods are static; the class holds no state and should not be instantiated. + * The default constructor binds to the current thread. The explicit constructor + * binds to a caller-provided OS thread ID (@ref Tid), allowing callers to act + * on library-owned background threads or any other live thread in the process. * - * @par Provided Queries + * @par Provided Queries / Operations * - @c hardware_concurrency() - delegates to @c std::thread::hardware_concurrency(). * - @c get_thread_id() - returns the OS-level thread ID (Linux TID via * @c syscall(SYS_gettid), Windows thread ID via @c GetCurrentThreadId()). - * - @c get_current_policy() - returns the calling thread's scheduling policy. - * On Windows this always returns @c SchedulingPolicy::OTHER. - * - @c get_current_priority() - returns the calling thread's scheduling priority. + * - instance methods provide @c set_name, @c get_name, @c set_priority, + * @c set_scheduling_policy, @c set_affinity, @c get_affinity, + * @c get_policy, and @c get_priority for the bound thread ID. + * - static @c get_current_policy() / @c get_current_priority() remain as + * compatibility conveniences for the current thread. * * @par Thread Safety - * All methods are thread-safe (they query per-thread or immutable system state). + * Individual operations are thread-safe and delegate to OS syscalls for the + * bound thread ID. */ class ThreadInfo { public: + ThreadInfo() : tid_(get_thread_id()) + { + } + + explicit ThreadInfo(Tid tid) : tid_(tid) + { + } + + [[nodiscard]] auto thread_id() const noexcept -> Tid + { + return tid_; + } + + [[nodiscard]] auto set_name(std::string const& name) const -> expected + { + return detail::apply_name(tid_, name); + } + + [[nodiscard]] auto get_name() const -> std::optional + { + return detail::read_name(tid_); + } + + [[nodiscard]] auto set_priority(ThreadPriority priority) const -> expected + { + return detail::apply_priority(tid_, priority); + } + + [[nodiscard]] auto set_scheduling_policy(SchedulingPolicy policy, ThreadPriority priority) const + -> expected + { + return detail::apply_scheduling_policy(tid_, policy, priority); + } + + [[nodiscard]] auto set_affinity(ThreadAffinity const& affinity) const -> expected + { + return detail::apply_affinity(tid_, affinity); + } + + [[nodiscard]] auto get_affinity() const -> std::optional + { + return detail::read_affinity(tid_); + } + + [[nodiscard]] auto get_policy() const -> std::optional + { + return detail::read_scheduling_policy(tid_); + } + + [[nodiscard]] auto get_priority() const -> std::optional + { + return detail::read_priority(tid_); + } + static auto hardware_concurrency() -> unsigned int { return std::thread::hardware_concurrency(); } - static auto get_thread_id() + static auto get_thread_id() -> Tid { #ifdef _WIN32 return GetCurrentThreadId(); @@ -838,39 +893,16 @@ class ThreadInfo static auto get_current_policy() -> std::optional { -#ifdef _WIN32 - // Windows doesn't have Linux-style scheduling policies - // Return OTHER as a default - return SchedulingPolicy::OTHER; -#else - const int policy = sched_getscheduler(0); - if (policy == -1) - { - return std::nullopt; - } - return static_cast(policy); -#endif + return ThreadInfo().get_policy(); } static auto get_current_priority() -> std::optional { -#ifdef _WIN32 - HANDLE thread = GetCurrentThread(); - int priority = GetThreadPriority(thread); - if (priority == THREAD_PRIORITY_ERROR_RETURN) - { - return std::nullopt; - } - return priority; -#else - sched_param param; - if (sched_getparam(0, ¶m) == 0) - { - return param.sched_priority; - } - return std::nullopt; -#endif + return ThreadInfo().get_priority(); } + + private: + Tid tid_{}; }; } // namespace threadschedule diff --git a/include/threadschedule/threadschedule.hpp b/include/threadschedule/threadschedule.hpp index 7ff2359..521dd4f 100644 --- a/include/threadschedule/threadschedule.hpp +++ b/include/threadschedule/threadschedule.hpp @@ -88,8 +88,10 @@ using ts::PollingWait; using ts::PoolWithErrors; using ts::TaskEndCallback; using ts::TaskStartCallback; +using ts::ThreadInfo; using ts::ThreadPriority; using ts::ThreadProfile; +using ts::Tid; using ts::ThreadWrapper; using ts::ThreadWrapperView; diff --git a/tests/thread_config_test.cpp b/tests/thread_config_test.cpp index 8de99a5..e268879 100644 --- a/tests/thread_config_test.cpp +++ b/tests/thread_config_test.cpp @@ -1,4 +1,5 @@ #include +#include #include using namespace threadschedule; @@ -261,6 +262,48 @@ TEST_F(ThreadConfigTest, ThreadConfigWithSchedulingPolicy) EXPECT_TRUE(executed); } +TEST_F(ThreadConfigTest, ThreadInfoDefaultConstructorTargetsCurrentThread) +{ + std::promise> ids; + + std::thread thread([&ids]() { + ThreadInfo info; + ids.set_value({info.thread_id(), ThreadInfo::get_thread_id()}); + }); + + auto const [from_constructor, from_static] = ids.get_future().get(); + thread.join(); + + EXPECT_EQ(from_constructor, from_static); +} + +TEST_F(ThreadConfigTest, ThreadInfoExplicitConstructorCanControlTargetThread) +{ + std::promise started; + std::promise release; + auto release_future = release.get_future().share(); + + std::thread thread([&started, release_future]() mutable { + started.set_value(ThreadInfo::get_thread_id()); + release_future.wait(); + }); + + Tid const tid = started.get_future().get(); + ThreadInfo info(tid); + + EXPECT_EQ(info.thread_id(), tid); + ASSERT_TRUE(info.set_name("ti_remote").has_value()); + + auto const name = info.get_name(); + ASSERT_TRUE(name.has_value()); + EXPECT_EQ(name.value(), "ti_remote"); + EXPECT_TRUE(info.get_policy().has_value()); + EXPECT_TRUE(info.get_priority().has_value()); + + release.set_value(); + thread.join(); +} + // ==================== Nice Value Tests ==================== TEST_F(ThreadConfigTest, NiceValue) diff --git a/tests/thread_pool_v2_test.cpp b/tests/thread_pool_v2_test.cpp index a2b3278..3eaadca 100644 --- a/tests/thread_pool_v2_test.cpp +++ b/tests/thread_pool_v2_test.cpp @@ -404,6 +404,20 @@ TEST(PoolV2, ScheduledInsertAfterShutdownReturnsCancelledHandle) EXPECT_TRUE(handle.is_cancelled()); } +TEST(PoolV2, ScheduledSchedulerThreadCanBeConfigured) +{ + ScheduledThreadPool scheduler(2); + + ASSERT_TRUE(scheduler.configure_scheduler_thread("sched_cfg").has_value()); + + auto info = scheduler.scheduler_thread_info(); + ASSERT_TRUE(info.has_value()); + + auto const name = info->get_name(); + ASSERT_TRUE(name.has_value()); + EXPECT_EQ(name.value(), "sched_cfg"); +} + TEST(PoolV2, ScheduledHPPool) { ScheduledHighPerformancePool scheduler(2); @@ -422,6 +436,25 @@ TEST(PoolV2, ScheduledLightweight) EXPECT_TRUE(ran); } +TEST(PoolV2, ChaosControllerThreadCanBeConfigured) +{ + ChaosConfig cfg; + cfg.interval = std::chrono::milliseconds(10); + cfg.shuffle_affinity = false; + cfg.priority_jitter = 0; + + ChaosController chaos(cfg, [](RegisteredThreadInfo const&) { return false; }); + + ASSERT_TRUE(chaos.configure_thread("chaos_cfg").has_value()); + + auto info = chaos.thread_info(); + ASSERT_TRUE(info.has_value()); + + auto const name = info->get_name(); + ASSERT_TRUE(name.has_value()); + EXPECT_EQ(name.value(), "chaos_cfg"); +} + // ==================== InlinePool ==================== TEST(PoolV2, InlinePoolSubmit) From 5f45a2c8be802427a718d3c05fa824d92b283065 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 29 May 2026 19:49:02 +0200 Subject: [PATCH 2/5] fix(docs): correct doxygen overload comment --- include/threadschedule/topology.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/threadschedule/topology.hpp b/include/threadschedule/topology.hpp index 457db1c..a804deb 100644 --- a/include/threadschedule/topology.hpp +++ b/include/threadschedule/topology.hpp @@ -185,7 +185,7 @@ inline auto affinity_for_node(int node_index, int thread_index, int threads_per_ /** * @brief Distribute thread affinities across NUMA nodes in round-robin order. * - * @overload Uses a pre-read topology to avoid repeated sysfs access. + * Uses a pre-read topology to avoid repeated sysfs access. * * @param topo Pre-read topology snapshot. * @param num_threads Number of affinity masks to generate. From fd847a275ba1599cfc398deb7a05faa5cee1d278 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 29 May 2026 20:09:17 +0200 Subject: [PATCH 3/5] feat(perf): modernize callable handling for newer C++ standards --- .github/workflows/tests.yml | 16 ++- benchmarks/CMakeLists.txt | 7 +- benchmarks/callable_benchmarks.cpp | 118 ++++++++++++++++++ include/threadschedule/callable.hpp | 49 ++++++++ include/threadschedule/error_handler.hpp | 48 +++++-- include/threadschedule/pthread_wrapper.hpp | 21 ++-- include/threadschedule/scheduled_pool.hpp | 105 +++++++++++++--- include/threadschedule/thread_pool.hpp | 106 ++++++++++++---- .../thread_pool_with_errors.hpp | 39 +++++- include/threadschedule/thread_registry.hpp | 35 +++++- tests/futures_test.cpp | 46 +++++++ tests/thread_config_test.cpp | 15 +++ tests/thread_pool_v2_test.cpp | 54 ++++++++ 13 files changed, 584 insertions(+), 75 deletions(-) create mode 100644 benchmarks/callable_benchmarks.cpp create mode 100644 include/threadschedule/callable.hpp diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 78a3a01..7e79a6d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,6 +37,7 @@ jobs: - { os: ubuntu-24.04, compiler: GCC 15, cc: gcc-15, cxx: g++-15, cpp_standard: 20 } - { os: ubuntu-24.04, compiler: GCC 15, cc: gcc-15, cxx: g++-15, cpp_standard: 23 } - { os: ubuntu-24.04, compiler: GCC 15, cc: gcc-15, cxx: g++-15, cpp_standard: 26 } + - { os: ubuntu-24.04, compiler: GCC 16, cc: gcc-16, cxx: g++-16, cpp_standard: 26 } - { os: ubuntu-24.04, compiler: Clang 16, cc: clang-16, cxx: clang++-16, cpp_standard: 17 } - { os: ubuntu-24.04, compiler: Clang 16, cc: clang-16, cxx: clang++-16, cpp_standard: 20 } - { os: ubuntu-24.04, compiler: Clang 18, cc: clang-18, cxx: clang++-18, cpp_standard: 17 } @@ -47,22 +48,27 @@ jobs: - { os: ubuntu-24.04, compiler: Clang 21, cc: clang-21, cxx: clang++-21, cpp_standard: 20 } - { os: ubuntu-24.04, compiler: Clang 21, cc: clang-21, cxx: clang++-21, cpp_standard: 23 } - { os: ubuntu-24.04, compiler: Clang 21, cc: clang-21, cxx: clang++-21, cpp_standard: 26 } + - { os: ubuntu-24.04, compiler: Clang 22, cc: clang-22, cxx: clang++-22, cpp_standard: 26 } steps: - name: Checkout code uses: actions/checkout@v4 - - name: Add toolchain PPA (GCC 15) - if: matrix.cc == 'gcc-15' + - name: Add toolchain PPA (GCC 15/16) + if: matrix.cc == 'gcc-15' || matrix.cc == 'gcc-16' run: | sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test sudo apt-get update - - name: Add LLVM repo (Clang 21) - if: matrix.cc == 'clang-21' + - name: Add LLVM repo (Clang 21/22) + if: matrix.cc == 'clang-21' || matrix.cc == 'clang-22' run: | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list + if [ "${{ matrix.cc }}" = "clang-22" ]; then + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm-22.list + else + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list + fi sudo apt-get update - name: Install dependencies diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 3e92da2..4feb43b 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable(threadpool_basic_benchmarks threadpool_benchmarks.cpp) add_executable(threadpool_throughput_benchmarks throughput_benchmarks.cpp) add_executable(threadpool_memory_benchmarks memory_benchmarks.cpp) add_executable(threadpool_resampling_benchmarks resampling_benchmarks.cpp) +add_executable(callable_benchmarks callable_benchmarks.cpp) # Real-world scenario benchmarks add_executable(web_server_benchmarks web_server_benchmarks.cpp) @@ -18,6 +19,7 @@ set(ALL_BENCHMARK_TARGETS threadpool_throughput_benchmarks threadpool_memory_benchmarks threadpool_resampling_benchmarks + callable_benchmarks web_server_benchmarks database_benchmarks audio_video_benchmarks @@ -56,6 +58,7 @@ add_test(NAME ThreadPoolBasicBenchmarks COMMAND threadpool_basic_benchmarks --be add_test(NAME ThreadPoolThroughputBenchmarks COMMAND threadpool_throughput_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) add_test(NAME ThreadPoolMemoryBenchmarks COMMAND threadpool_memory_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) add_test(NAME ThreadPoolResamplingBenchmarks COMMAND threadpool_resampling_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) +add_test(NAME CallableBenchmarks COMMAND callable_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) add_test(NAME WebServerBenchmarks COMMAND web_server_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) add_test(NAME DatabaseBenchmarks COMMAND database_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) add_test(NAME AudioVideoBenchmarks COMMAND audio_video_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3) @@ -70,7 +73,9 @@ add_custom_target(run_core_benchmarks COMMAND threadpool_throughput_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3 COMMAND echo "=== Memory Benchmarks ===" COMMAND threadpool_memory_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3 - DEPENDS threadpool_basic_benchmarks threadpool_throughput_benchmarks threadpool_memory_benchmarks + COMMAND echo "=== Callable Benchmarks ===" + COMMAND callable_benchmarks --benchmark_min_time=2s --benchmark_repetitions=3 + DEPENDS threadpool_basic_benchmarks threadpool_throughput_benchmarks threadpool_memory_benchmarks callable_benchmarks COMMENT "Running core ThreadSchedule benchmarks (explicit target, not part of normal build)" ) diff --git a/benchmarks/callable_benchmarks.cpp b/benchmarks/callable_benchmarks.cpp new file mode 100644 index 0000000..6f43a9c --- /dev/null +++ b/benchmarks/callable_benchmarks.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include + +using namespace threadschedule; + +namespace +{ + +template +void wait_for_posts(Pool& pool) +{ + if constexpr (requires { pool.wait_for_tasks(); }) + { + pool.wait_for_tasks(); + } +} + +template +void run_post_benchmark(benchmark::State& state, SubmitFn&& submit) +{ + Pool pool(static_cast(state.range(0))); + std::atomic completed{0}; + size_t const task_count = static_cast(state.range(1)); + + for (auto _ : state) + { + completed.store(0, std::memory_order_relaxed); + for (size_t i = 0; i < task_count; ++i) + { + submit(pool, completed); + } + + while (completed.load(std::memory_order_acquire) < task_count) + std::this_thread::yield(); + + wait_for_posts(pool); + } + + state.SetItemsProcessed(state.iterations() * task_count); +} + +} // namespace + +static void BM_ThreadPool_PostSmallCapture(benchmark::State& state) +{ + run_post_benchmark(state, [](ThreadPool& pool, std::atomic& completed) { + pool.post([&completed]() { completed.fetch_add(1, std::memory_order_relaxed); }); + }); +} + +static void BM_ThreadPool_PostLargeCapture(benchmark::State& state) +{ + run_post_benchmark(state, [](ThreadPool& pool, std::atomic& completed) { + std::array payload{}; + payload[0] = 7; + pool.post([payload, &completed]() { + completed.fetch_add(payload[0] == 7 ? 1u : 0u, std::memory_order_relaxed); + }); + }); +} + +static void BM_HighPerformancePool_PostSmallCapture(benchmark::State& state) +{ + run_post_benchmark(state, [](HighPerformancePool& pool, std::atomic& completed) { + pool.post([&completed]() { completed.fetch_add(1, std::memory_order_relaxed); }); + }); +} + +static void BM_HighPerformancePool_PostLargeCapture(benchmark::State& state) +{ + run_post_benchmark(state, [](HighPerformancePool& pool, std::atomic& completed) { + std::array payload{}; + payload[0] = 11; + pool.post([payload, &completed]() { + completed.fetch_add(payload[0] == 11 ? 1u : 0u, std::memory_order_relaxed); + }); + }); +} + +#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L +static void BM_ThreadPool_PostMoveOnlyCapture(benchmark::State& state) +{ + run_post_benchmark(state, [](ThreadPool& pool, std::atomic& completed) { + auto payload = std::make_unique(123); + pool.post([payload = std::move(payload), &completed]() mutable { + benchmark::DoNotOptimize(*payload); + completed.fetch_add(1, std::memory_order_relaxed); + }); + }); +} + +static void BM_HighPerformancePool_PostMoveOnlyCapture(benchmark::State& state) +{ + run_post_benchmark(state, [](HighPerformancePool& pool, std::atomic& completed) { + auto payload = std::make_unique(456); + pool.post([payload = std::move(payload), &completed]() mutable { + benchmark::DoNotOptimize(*payload); + completed.fetch_add(1, std::memory_order_relaxed); + }); + }); +} +#endif + +BENCHMARK(BM_ThreadPool_PostSmallCapture)->Args({2, 1000})->Args({4, 10000}); +BENCHMARK(BM_ThreadPool_PostLargeCapture)->Args({2, 1000})->Args({4, 10000}); +BENCHMARK(BM_HighPerformancePool_PostSmallCapture)->Args({2, 1000})->Args({4, 10000}); +BENCHMARK(BM_HighPerformancePool_PostLargeCapture)->Args({2, 1000})->Args({4, 10000}); + +#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L +BENCHMARK(BM_ThreadPool_PostMoveOnlyCapture)->Args({2, 1000})->Args({4, 10000}); +BENCHMARK(BM_HighPerformancePool_PostMoveOnlyCapture)->Args({2, 1000})->Args({4, 10000}); +#endif + +BENCHMARK_MAIN(); diff --git a/include/threadschedule/callable.hpp b/include/threadschedule/callable.hpp new file mode 100644 index 0000000..3474b56 --- /dev/null +++ b/include/threadschedule/callable.hpp @@ -0,0 +1,49 @@ +#pragma once + +/** + * @file callable.hpp + * @brief Feature-gated callable storage aliases for modern C++ builds. + */ + +#include +#include +#include + +namespace threadschedule +{ +namespace detail +{ + +template +using remove_cvref_t = std::remove_cv_t>; + +#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L +template +using move_callable = std::move_only_function; +#else +template +using move_callable = std::function; +#endif + +#if defined(__cpp_lib_copyable_function) && __cpp_lib_copyable_function >= 202306L +template +using copyable_callable = std::copyable_function; +#else +template +using copyable_callable = std::function; +#endif + +template +auto make_move_callable(Callable&& callable) -> move_callable +{ + return move_callable(std::forward(callable)); +} + +template +auto make_copyable_callable(Callable&& callable) -> copyable_callable +{ + return copyable_callable(std::forward(callable)); +} + +} // namespace detail +} // namespace threadschedule diff --git a/include/threadschedule/error_handler.hpp b/include/threadschedule/error_handler.hpp index 3b9e50d..11d6484 100644 --- a/include/threadschedule/error_handler.hpp +++ b/include/threadschedule/error_handler.hpp @@ -5,6 +5,7 @@ * @brief Error handling primitives: TaskError, ErrorHandler, and ErrorHandledTask. */ +#include "callable.hpp" #include #include #include @@ -115,6 +116,9 @@ struct TaskError */ using ErrorCallback = std::function; +using ErrorCallbackStorage = detail::copyable_callable; +using FutureErrorCallback = detail::move_callable; + /** * @brief Central registry and dispatcher for task-error callbacks. * @@ -150,10 +154,16 @@ class ErrorHandler */ auto add_callback(ErrorCallback callback) -> size_t { - std::lock_guard lock(mutex_); - size_t const id = next_callback_id_++; - callbacks_.emplace(id, std::move(callback)); - return id; + return emplace_callback(ErrorCallbackStorage(std::move(callback))); + } + + template , ErrorCallback>, int> = 0> + auto add_callback(Callback&& callback) -> size_t + { + static_assert(std::is_invocable_r_v, + "Error callback must be invocable with TaskError const&"); + return emplace_callback(detail::make_copyable_callable(std::forward(callback))); } /** @@ -200,7 +210,7 @@ class ErrorHandler */ void handle_error(TaskError const& error) { - std::vector snapshot; + std::vector snapshot; { std::lock_guard lock(mutex_); error_count_++; @@ -209,7 +219,7 @@ class ErrorHandler snapshot.push_back(callback); } - for (auto const& callback : snapshot) + for (auto& callback : snapshot) { try { @@ -245,8 +255,16 @@ class ErrorHandler } private: + auto emplace_callback(ErrorCallbackStorage callback) -> size_t + { + std::lock_guard lock(mutex_); + size_t const id = next_callback_id_++; + callbacks_.emplace(id, std::move(callback)); + return id; + } + mutable std::mutex mutex_; - std::map callbacks_; + std::map callbacks_; size_t next_callback_id_{0}; size_t error_count_{0}; }; @@ -357,7 +375,19 @@ class FutureWithErrorHandler */ auto on_error(std::function callback) -> FutureWithErrorHandler& { - error_callback_ = std::move(callback); + error_callback_ = FutureErrorCallback(std::move(callback)); + has_callback_ = true; + return *this; + } + + template , std::function>, + int> = 0> + auto on_error(Callback&& callback) -> FutureWithErrorHandler& + { + static_assert(std::is_invocable_r_v, + "Error callback must be invocable with std::exception_ptr"); + error_callback_ = detail::make_move_callable(std::forward(callback)); has_callback_ = true; return *this; } @@ -438,7 +468,7 @@ class FutureWithErrorHandler private: std::future future_; - std::function error_callback_; + FutureErrorCallback error_callback_; bool has_callback_{false}; }; diff --git a/include/threadschedule/pthread_wrapper.hpp b/include/threadschedule/pthread_wrapper.hpp index 51816de..8731321 100644 --- a/include/threadschedule/pthread_wrapper.hpp +++ b/include/threadschedule/pthread_wrapper.hpp @@ -5,6 +5,7 @@ * @brief RAII wrapper around POSIX threads (Linux only). */ +#include "callable.hpp" #include "concepts.hpp" #include "expected.hpp" #include "scheduler_policy.hpp" @@ -28,6 +29,8 @@ namespace threadschedule { #ifndef _WIN32 +using PThreadEntryPoint = detail::move_callable; + /** * @brief RAII wrapper around POSIX threads with a modern C++ interface. * @@ -66,10 +69,10 @@ class PThreadWrapper { auto callable = - std::make_unique>([fn = std::forward(func), - tup = std::make_tuple(std::forward(args)...)]() mutable { - std::apply(std::move(fn), std::move(tup)); - }); + std::make_unique(detail::make_move_callable( + [fn = std::forward(func), tup = std::make_tuple(std::forward(args)...)]() mutable { + std::apply(std::move(fn), std::move(tup)); + })); int const result = pthread_create(&thread_, nullptr, thread_function, callable.release()); @@ -229,10 +232,10 @@ class PThreadWrapper PThreadWrapper wrapper; auto callable = - std::make_unique>([fn = std::forward(func), - tup = std::make_tuple(std::forward(args)...)]() mutable { - std::apply(std::move(fn), std::move(tup)); - }); + std::make_unique(detail::make_move_callable( + [fn = std::forward(func), tup = std::make_tuple(std::forward(args)...)]() mutable { + std::apply(std::move(fn), std::move(tup)); + })); int const result = pthread_create(&wrapper.thread_, &attr, thread_function, callable.release()); @@ -250,7 +253,7 @@ class PThreadWrapper static auto thread_function(void* arg) -> void* { - std::unique_ptr> func(static_cast*>(arg)); + std::unique_ptr func(static_cast(arg)); try { diff --git a/include/threadschedule/scheduled_pool.hpp b/include/threadschedule/scheduled_pool.hpp index 2f57f7f..e14a39d 100644 --- a/include/threadschedule/scheduled_pool.hpp +++ b/include/threadschedule/scheduled_pool.hpp @@ -140,6 +140,8 @@ class ScheduledThreadPoolT { public: using Task = std::function; + using OneShotTask = detail::move_callable; + using PeriodicTask = detail::copyable_callable; using TimePoint = std::chrono::steady_clock::time_point; using Duration = std::chrono::steady_clock::duration; @@ -148,7 +150,8 @@ class ScheduledThreadPoolT uint64_t id; TimePoint next_run; Duration interval; // Zero for one-time tasks - Task task; + OneShotTask one_shot_task; + std::shared_ptr periodic_task; std::shared_ptr> cancelled; bool periodic; }; @@ -189,7 +192,14 @@ class ScheduledThreadPoolT auto schedule_after(Duration delay, Task task) -> ScheduledTaskHandle { auto run_time = std::chrono::steady_clock::now() + delay; - return schedule_at(run_time, std::move(task)); + return insert_one_shot_task(run_time, detail::make_move_callable(std::move(task))); + } + + template , Task>, int> = 0> + auto schedule_after(Duration delay, F&& task) -> ScheduledTaskHandle + { + auto run_time = std::chrono::steady_clock::now() + delay; + return insert_one_shot_task(run_time, detail::make_move_callable(std::forward(task))); } /** @@ -200,7 +210,13 @@ class ScheduledThreadPoolT */ auto schedule_at(TimePoint time_point, Task task) -> ScheduledTaskHandle { - return insert_task(time_point, Duration::zero(), std::move(task), false); + return insert_one_shot_task(time_point, detail::make_move_callable(std::move(task))); + } + + template , Task>, int> = 0> + auto schedule_at(TimePoint time_point, F&& task) -> ScheduledTaskHandle + { + return insert_one_shot_task(time_point, detail::make_move_callable(std::forward(task))); } /** @@ -217,6 +233,12 @@ class ScheduledThreadPoolT return schedule_periodic_after(Duration::zero(), interval, std::move(task)); } + template , Task>, int> = 0> + auto schedule_periodic(Duration interval, F&& task) -> ScheduledTaskHandle + { + return schedule_periodic_after(Duration::zero(), interval, std::forward(task)); + } + /** * @brief Schedule a task to run periodically after an initial delay * @param initial_delay Duration to wait before first execution @@ -227,7 +249,15 @@ class ScheduledThreadPoolT auto schedule_periodic_after(Duration initial_delay, Duration interval, Task task) -> ScheduledTaskHandle { auto const run_time = std::chrono::steady_clock::now() + initial_delay; - return insert_task(run_time, interval, std::move(task), true); + return insert_periodic_task(run_time, interval, detail::make_copyable_callable(std::move(task))); + } + + template , Task>, int> = 0> + auto schedule_periodic_after(Duration initial_delay, Duration interval, F&& task) -> ScheduledTaskHandle + { + auto const run_time = std::chrono::steady_clock::now() + initial_delay; + return insert_periodic_task(run_time, interval, + detail::make_copyable_callable(std::forward(task))); } /** @@ -320,7 +350,34 @@ class ScheduledThreadPoolT std::multimap scheduled_tasks_; std::atomic next_task_id_; - auto insert_task(TimePoint run_time, Duration interval, Task task, bool periodic) -> ScheduledTaskHandle + auto insert_one_shot_task(TimePoint run_time, OneShotTask task) -> ScheduledTaskHandle + { + std::lock_guard lock(mutex_); + + uint64_t const task_id = next_task_id_++; + ScheduledTaskHandle handle(task_id); + + if (stop_) + { + handle.cancel(); + return handle; + } + + ScheduledTaskInfo info; + info.id = task_id; + info.next_run = run_time; + info.interval = Duration::zero(); + info.one_shot_task = std::move(task); + info.cancelled = handle.get_cancel_flag(); + info.periodic = false; + + scheduled_tasks_.insert({run_time, std::move(info)}); + condition_.notify_one(); + + return handle; + } + + auto insert_periodic_task(TimePoint run_time, Duration interval, PeriodicTask task) -> ScheduledTaskHandle { std::lock_guard lock(mutex_); @@ -337,9 +394,9 @@ class ScheduledThreadPoolT info.id = task_id; info.next_run = run_time; info.interval = interval; - info.task = std::move(task); + info.periodic_task = std::make_shared(std::move(task)); info.cancelled = handle.get_cancel_flag(); - info.periodic = periodic; + info.periodic = true; scheduled_tasks_.insert({run_time, std::move(info)}); condition_.notify_one(); @@ -395,22 +452,32 @@ class ScheduledThreadPoolT // Schedule for execution in the thread pool try { - // Capture task and periodic info - auto task_copy = info.task; auto cancelled_flag = info.cancelled; - - pool_.post([task_copy, cancelled_flag]() { - if (!cancelled_flag->load(std::memory_order_acquire)) + if (info.periodic) + { + auto periodic_task = info.periodic_task; + pool_.post([periodic_task, cancelled_flag]() { + if (!cancelled_flag->load(std::memory_order_acquire)) + { + (*periodic_task)(); + } + }); + + if (!info.cancelled->load(std::memory_order_acquire)) { - task_copy(); + info.next_run += info.interval; + scheduled_tasks_.insert({info.next_run, std::move(info)}); } - }); - - // Reschedule if periodic - if (info.periodic && !info.cancelled->load(std::memory_order_acquire)) + } + else { - info.next_run += info.interval; - scheduled_tasks_.insert({info.next_run, std::move(info)}); + auto one_shot_task = std::move(info.one_shot_task); + pool_.post([task = std::move(one_shot_task), cancelled_flag]() mutable { + if (!cancelled_flag->load(std::memory_order_acquire)) + { + task(); + } + }); } } catch (...) diff --git a/include/threadschedule/thread_pool.hpp b/include/threadschedule/thread_pool.hpp index acc744f..7f769a8 100644 --- a/include/threadschedule/thread_pool.hpp +++ b/include/threadschedule/thread_pool.hpp @@ -5,6 +5,7 @@ * @brief Thread pools: HighPerformancePool, ThreadPoolBase, LightweightPoolT, and GlobalPool. */ +#include "callable.hpp" #include "expected.hpp" #include "scheduler_policy.hpp" #include "thread_registry.hpp" @@ -20,6 +21,7 @@ #include #include #include +#include #include #include @@ -323,6 +325,11 @@ using TaskStartCallback = std::function; +using TaskStartCallbackStorage = + detail::copyable_callable; +using TaskEndCallbackStorage = + detail::copyable_callable; + template class WorkStealingDeque { @@ -338,6 +345,7 @@ class WorkStealingDeque AlignedItem(T&& t) : item(std::move(t)) { } + template , int> = 0> AlignedItem(T const& t) : item(t) { } @@ -372,6 +380,7 @@ class WorkStealingDeque return true; } + template , int> = 0> [[nodiscard]] auto push(T const& item) -> bool { std::lock_guard lock(mutex_); @@ -556,6 +565,7 @@ class HighPerformancePool { public: using Task = std::function; + using QueuedTask = detail::move_callable; struct Statistics { @@ -569,7 +579,7 @@ class HighPerformancePool }; explicit HighPerformancePool(size_t num_threads = std::thread::hardware_concurrency(), - size_t deque_capacity = WorkStealingDeque::DEFAULT_CAPACITY, + size_t deque_capacity = WorkStealingDeque::DEFAULT_CAPACITY, bool register_workers = false) : num_threads_(num_threads == 0 ? 1 : num_threads), register_workers_(register_workers), stop_(false), next_victim_(0), start_time_(std::chrono::steady_clock::now()) @@ -577,7 +587,7 @@ class HighPerformancePool worker_queues_.resize(num_threads_); for (size_t i = 0; i < num_threads_; ++i) { - worker_queues_[i] = std::make_unique>(deque_capacity); + worker_queues_[i] = std::make_unique>(deque_capacity); } workers_.reserve(num_threads_); @@ -613,7 +623,7 @@ class HighPerformancePool if (policy == ShutdownPolicy::drop_pending) { dropped_tasks += overflow_tasks_.size(); - std::queue empty; + std::queue empty; overflow_tasks_.swap(empty); for (auto& q : worker_queues_) dropped_tasks += q->clear_and_count(); @@ -771,7 +781,8 @@ class HighPerformancePool template auto try_post(F&& f, Args&&... args) -> expected { - Task bound(detail::bind_args(std::forward(f), std::forward(args)...)); + QueuedTask bound(detail::make_move_callable( + detail::bind_args(std::forward(f), std::forward(args)...))); if (stop_.load(std::memory_order_acquire)) return unexpected(std::make_error_code(std::errc::operation_canceled)); @@ -1056,7 +1067,18 @@ class HighPerformancePool void set_on_task_start(TaskStartCallback cb) { std::lock_guard lock(trace_mutex_); - on_task_start_ = std::move(cb); + on_task_start_ = TaskStartCallbackStorage(std::move(cb)); + } + + template , TaskStartCallback>, int> = 0> + void set_on_task_start(Callback&& cb) + { + static_assert(std::is_invocable_r_v, + "Task start callback must accept (time_point, std::thread::id)"); + std::lock_guard lock(trace_mutex_); + on_task_start_ = detail::make_copyable_callable( + std::forward(cb)); } /** @@ -1067,7 +1089,21 @@ class HighPerformancePool void set_on_task_end(TaskEndCallback cb) { std::lock_guard lock(trace_mutex_); - on_task_end_ = std::move(cb); + on_task_end_ = TaskEndCallbackStorage(std::move(cb)); + } + + template , TaskEndCallback>, int> = 0> + void set_on_task_end(Callback&& cb) + { + static_assert( + std::is_invocable_r_v, + "Task end callback must accept (time_point, std::thread::id, std::chrono::microseconds)"); + std::lock_guard lock(trace_mutex_); + on_task_end_ = + detail::make_copyable_callable(std::forward(cb)); } /// @} @@ -1076,9 +1112,9 @@ class HighPerformancePool size_t num_threads_; bool register_workers_; std::vector workers_; - std::vector>> worker_queues_; + std::vector>> worker_queues_; - std::queue overflow_tasks_; + std::queue overflow_tasks_; mutable std::mutex overflow_mutex_; std::atomic stop_; @@ -1096,8 +1132,8 @@ class HighPerformancePool std::atomic total_task_time_{0}; std::mutex trace_mutex_; - TaskStartCallback on_task_start_; - TaskEndCallback on_task_end_; + TaskStartCallbackStorage on_task_start_; + TaskEndCallbackStorage on_task_end_; std::chrono::steady_clock::time_point start_time_; @@ -1113,7 +1149,7 @@ class HighPerformancePool return std::mt19937(device()); }(); - Task task; + QueuedTask task; std::uniform_int_distribution dist(0, num_threads_ - 1); while (true) @@ -1157,7 +1193,7 @@ class HighPerformancePool auto const start_time = std::chrono::steady_clock::now(); auto const tid = std::this_thread::get_id(); - TaskStartCallback on_task_start; + TaskStartCallbackStorage on_task_start; { std::lock_guard tl(trace_mutex_); on_task_start = on_task_start_; @@ -1182,7 +1218,7 @@ class HighPerformancePool auto const task_duration = std::chrono::duration_cast(end_time - start_time); total_task_time_.fetch_add(task_duration.count(), std::memory_order_relaxed); - TaskEndCallback on_task_end; + TaskEndCallbackStorage on_task_end; { std::lock_guard tl(trace_mutex_); on_task_end = on_task_end_; @@ -1315,6 +1351,7 @@ class ThreadPoolBase { public: using Task = std::function; + using QueuedTask = detail::move_callable; struct Statistics { @@ -1593,7 +1630,7 @@ class ThreadPoolBase stop_ = true; if (policy == ShutdownPolicy::drop_pending) { - std::queue empty; + std::queue empty; tasks_.swap(empty); } } @@ -1686,7 +1723,18 @@ class ThreadPoolBase void set_on_task_start(TaskStartCallback cb) { std::lock_guard lock(trace_mutex_); - on_task_start_ = std::move(cb); + on_task_start_ = TaskStartCallbackStorage(std::move(cb)); + } + + template , TaskStartCallback>, int> = 0> + void set_on_task_start(Callback&& cb) + { + static_assert(std::is_invocable_r_v, + "Task start callback must accept (time_point, std::thread::id)"); + std::lock_guard lock(trace_mutex_); + on_task_start_ = detail::make_copyable_callable( + std::forward(cb)); } /** @@ -1697,7 +1745,21 @@ class ThreadPoolBase void set_on_task_end(TaskEndCallback cb) { std::lock_guard lock(trace_mutex_); - on_task_end_ = std::move(cb); + on_task_end_ = TaskEndCallbackStorage(std::move(cb)); + } + + template , TaskEndCallback>, int> = 0> + void set_on_task_end(Callback&& cb) + { + static_assert( + std::is_invocable_r_v, + "Task end callback must accept (time_point, std::thread::id, std::chrono::microseconds)"); + std::lock_guard lock(trace_mutex_); + on_task_end_ = + detail::make_copyable_callable(std::forward(cb)); } /// @} @@ -1706,7 +1768,7 @@ class ThreadPoolBase size_t num_threads_; bool register_workers_; std::vector workers_; - std::queue tasks_; + std::queue tasks_; mutable std::mutex queue_mutex_; std::condition_variable condition_; @@ -1717,8 +1779,8 @@ class ThreadPoolBase std::atomic total_task_time_{0}; std::mutex trace_mutex_; - TaskStartCallback on_task_start_; - TaskEndCallback on_task_end_; + TaskStartCallbackStorage on_task_start_; + TaskEndCallbackStorage on_task_end_; std::chrono::steady_clock::time_point start_time_; @@ -1730,7 +1792,7 @@ class ThreadPoolBase while (true) { - Task task; + QueuedTask task; bool found_task = false; { @@ -1762,7 +1824,7 @@ class ThreadPoolBase auto const start_time = std::chrono::steady_clock::now(); auto const tid = std::this_thread::get_id(); - TaskStartCallback on_task_start; + TaskStartCallbackStorage on_task_start; { std::lock_guard tl(trace_mutex_); on_task_start = on_task_start_; @@ -1783,7 +1845,7 @@ class ThreadPoolBase auto const task_duration = std::chrono::duration_cast(end_time - start_time); total_task_time_.fetch_add(task_duration.count(), std::memory_order_relaxed); - TaskEndCallback on_task_end; + TaskEndCallbackStorage on_task_end; { std::lock_guard tl(trace_mutex_); on_task_end = on_task_end_; diff --git a/include/threadschedule/thread_pool_with_errors.hpp b/include/threadschedule/thread_pool_with_errors.hpp index 2161269..413c660 100644 --- a/include/threadschedule/thread_pool_with_errors.hpp +++ b/include/threadschedule/thread_pool_with_errors.hpp @@ -90,6 +90,13 @@ class PoolWithErrors return error_handler_->add_callback(std::move(callback)); } + template , ErrorCallback>, int> = 0> + auto add_error_callback(Callback&& callback) -> size_t + { + return error_handler_->add_callback(std::forward(callback)); + } + auto remove_error_callback(size_t id) -> bool { return error_handler_->remove_callback(id); @@ -161,12 +168,22 @@ class PoolWithErrors auto submit_impl(std::string description, F&& f, Args&&... args) -> FutureWithErrorHandler> { + using return_type = std::invoke_result_t; auto handler = error_handler_; - auto wrapped_task = [f = std::forward(f), args = std::make_tuple(std::forward(args)...), handler, - desc = std::move(description)]() { + auto wrapped_task = + [bound = detail::bind_args(std::forward(f), std::forward(args)...), handler, + desc = std::move(description)]() mutable -> return_type { try { - return std::apply(f, args); + if constexpr (std::is_void_v) + { + bound(); + return; + } + else + { + return bound(); + } } catch (...) { @@ -182,12 +199,22 @@ class PoolWithErrors auto try_submit_impl(std::string description, F&& f, Args&&... args) -> expected>, std::error_code> { + using return_type = std::invoke_result_t; auto handler = error_handler_; - auto wrapped_task = [f = std::forward(f), args = std::make_tuple(std::forward(args)...), handler, - desc = std::move(description)]() { + auto wrapped_task = + [bound = detail::bind_args(std::forward(f), std::forward(args)...), handler, + desc = std::move(description)]() mutable -> return_type { try { - return std::apply(f, args); + if constexpr (std::is_void_v) + { + bound(); + return; + } + else + { + return bound(); + } } catch (...) { diff --git a/include/threadschedule/thread_registry.hpp b/include/threadschedule/thread_registry.hpp index 99d4c26..5dd1b36 100644 --- a/include/threadschedule/thread_registry.hpp +++ b/include/threadschedule/thread_registry.hpp @@ -5,6 +5,7 @@ * @brief Process-wide thread registry, control blocks, and composite registry. */ +#include "callable.hpp" #include "expected.hpp" #include "scheduler_policy.hpp" #include "thread_wrapper.hpp" // for ThreadInfo, ThreadAffinity @@ -87,6 +88,8 @@ struct RegisteredThreadInfo std::shared_ptr control; }; +using RegistryCallback = detail::copyable_callable; + /** * @brief Per-thread control handle for OS-level scheduling operations. * @@ -602,13 +605,37 @@ class ThreadRegistry : public detail::QueryFacadeMixin void set_on_register(std::function cb) { std::unique_lock lock(mutex_); - onRegister_ = std::move(cb); + onRegister_ = RegistryCallback(std::move(cb)); + } + + template , + std::function>, + int> = 0> + void set_on_register(Callback&& cb) + { + static_assert(std::is_invocable_r_v, + "Register callback must be invocable with RegisteredThreadInfo const&"); + std::unique_lock lock(mutex_); + onRegister_ = detail::make_copyable_callable(std::forward(cb)); } void set_on_unregister(std::function cb) { std::unique_lock lock(mutex_); - onUnregister_ = std::move(cb); + onUnregister_ = RegistryCallback(std::move(cb)); + } + + template , + std::function>, + int> = 0> + void set_on_unregister(Callback&& cb) + { + static_assert(std::is_invocable_r_v, + "Unregister callback must be invocable with RegisteredThreadInfo const&"); + std::unique_lock lock(mutex_); + onUnregister_ = detail::make_copyable_callable(std::forward(cb)); } private: @@ -639,8 +666,8 @@ class ThreadRegistry : public detail::QueryFacadeMixin mutable std::shared_mutex mutex_; std::unordered_map threads_; - std::function onRegister_; - std::function onUnregister_; + RegistryCallback onRegister_; + RegistryCallback onUnregister_; }; /** diff --git a/tests/futures_test.cpp b/tests/futures_test.cpp index 661889d..6e511e2 100644 --- a/tests/futures_test.cpp +++ b/tests/futures_test.cpp @@ -1,7 +1,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -174,3 +177,46 @@ TEST(FuturesTest, WhenAnyVoidPropagatesException) EXPECT_THROW(when_any(futures), std::runtime_error); } + +TEST(FuturesTest, PoolWithErrorsInvokesRegisteredCallback) +{ + ThreadPoolWithErrors pool(1); + std::promise error_seen; + auto reported = error_seen.get_future(); + + pool.add_error_callback([&error_seen](TaskError const& error) { error_seen.set_value(error.what()); }); + + auto future = pool.submit([]() -> int { throw std::runtime_error("boom"); }); + EXPECT_THROW(future.get(), std::runtime_error); + EXPECT_EQ(reported.get(), "boom"); +} + +TEST(FuturesTest, PoolWithErrorsSubmitAcceptsMoveOnlyArguments) +{ + ThreadPoolWithErrors pool(1); + auto payload = std::make_unique("wrapped"); + + auto future = + pool.submit([](std::unique_ptr value) { return value == nullptr ? std::string() : *value; }, + std::move(payload)); + + EXPECT_EQ(future.get(), "wrapped"); +} + +#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L +TEST(FuturesTest, FutureWithErrorHandlerAcceptsMoveOnlyErrorCallback) +{ + ThreadPoolWithErrors pool(1); + auto callback_done = std::make_unique>(); + auto callback_seen = callback_done->get_future(); + + auto future = pool.submit([]() -> int { throw std::runtime_error("move-only"); }); + future.on_error([done = std::move(callback_done)](std::exception_ptr error) mutable { + EXPECT_THROW(std::rethrow_exception(error), std::runtime_error); + done->set_value(); + }); + + EXPECT_THROW(future.get(), std::runtime_error); + EXPECT_EQ(callback_seen.wait_for(std::chrono::seconds(1)), std::future_status::ready); +} +#endif diff --git a/tests/thread_config_test.cpp b/tests/thread_config_test.cpp index e268879..1071d5f 100644 --- a/tests/thread_config_test.cpp +++ b/tests/thread_config_test.cpp @@ -304,6 +304,21 @@ TEST_F(ThreadConfigTest, ThreadInfoExplicitConstructorCanControlTargetThread) thread.join(); } +TEST_F(ThreadConfigTest, ThreadInfoInvalidTargetReturnsNoProcess) +{ +#ifdef _WIN32 + ThreadInfo info(Tid{0}); +#else + ThreadInfo info(static_cast(-1)); +#endif + + EXPECT_FALSE(info.get_name().has_value()); + EXPECT_FALSE(info.get_affinity().has_value()); + EXPECT_FALSE(info.get_policy().has_value()); + EXPECT_FALSE(info.get_priority().has_value()); + EXPECT_FALSE(info.set_name("invalid_tid").has_value()); +} + // ==================== Nice Value Tests ==================== TEST_F(ThreadConfigTest, NiceValue) diff --git a/tests/thread_pool_v2_test.cpp b/tests/thread_pool_v2_test.cpp index 3eaadca..5547df2 100644 --- a/tests/thread_pool_v2_test.cpp +++ b/tests/thread_pool_v2_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,36 @@ TEST(PoolV2, PostThrowsOnShutdown) EXPECT_THROW(pool.post([] {}), std::runtime_error); } +#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L +TEST(PoolV2, ThreadPoolTryPostAcceptsMoveOnlyTask) +{ + ThreadPool pool(2); + auto payload = std::make_unique(42); + std::promise done; + auto finished = done.get_future(); + + auto result = pool.try_post([payload = std::move(payload), &done]() mutable { done.set_value(*payload); }); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(finished.get(), 42); + pool.shutdown(ShutdownPolicy::drain); +} + +TEST(PoolV2, HighPerformancePoolTryPostAcceptsMoveOnlyTask) +{ + HighPerformancePool pool(2); + auto payload = std::make_unique(77); + std::promise done; + auto finished = done.get_future(); + + auto result = pool.try_post([payload = std::move(payload), &done]() mutable { done.set_value(*payload); }); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(finished.get(), 77); + pool.shutdown(ShutdownPolicy::drain); +} +#endif + // ==================== submit_batch / try_submit_batch ==================== TEST(PoolV2, SubmitBatchExecutesAll) @@ -418,6 +449,29 @@ TEST(PoolV2, ScheduledSchedulerThreadCanBeConfigured) EXPECT_EQ(name.value(), "sched_cfg"); } +TEST(PoolV2, ScheduledSchedulerThreadInfoUnavailableAfterShutdown) +{ + ScheduledThreadPool scheduler(2); + scheduler.shutdown(); + EXPECT_FALSE(scheduler.scheduler_thread_info().has_value()); +} + +#if defined(__cpp_lib_move_only_function) && __cpp_lib_move_only_function >= 202110L +TEST(PoolV2, ScheduledAfterAcceptsMoveOnlyTask) +{ + ScheduledThreadPool scheduler(2); + auto payload = std::make_unique(55); + std::promise done; + auto finished = done.get_future(); + + scheduler.schedule_after(std::chrono::milliseconds(20), + [payload = std::move(payload), &done]() mutable { done.set_value(*payload); }); + + EXPECT_EQ(finished.wait_for(std::chrono::seconds(2)), std::future_status::ready); + EXPECT_EQ(finished.get(), 55); +} +#endif + TEST(PoolV2, ScheduledHPPool) { ScheduledHighPerformancePool scheduler(2); From b53977002b47fed61113fce0a3fae91afb26b809 Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 29 May 2026 20:12:38 +0200 Subject: [PATCH 4/5] docs(changelog): document callable modernization updates --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed961a3..ffdc9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,47 @@ receive stable default names, keeping thread-control behavior consistent across the library. (`scheduled_pool.hpp`, `chaos.hpp`) +- **Callable storage is now feature-gated by language/library support** -- + internal task and callback paths use modern standard call wrappers when they + are available: move-only task queues can use `std::move_only_function` + (C++23+ libraries), reusable hooks/callbacks can use + `std::copyable_function` (C++26-capable libraries), and older standards keep + the `std::function` fallback. Public aliases remain source-compatible while + new templated setter/registration overloads avoid unnecessary type-erasure + constraints. (`callable.hpp`, `thread_pool.hpp`, `scheduled_pool.hpp`, + `error_handler.hpp`, `thread_registry.hpp`, `thread_pool_with_errors.hpp`, + `pthread_wrapper.hpp`) + +### Performance + +- **Move-only tasks are now supported on more hot paths** -- `post`/`try_post` + and scheduler one-shot dispatch can carry move-only captures directly instead + of forcing a copyable `std::function` path on newer standard libraries. + This reduces adaptation overhead for fire-and-forget workloads and enables + more modern task payloads without wrapper glue. (`thread_pool.hpp`, + `scheduled_pool.hpp`, `thread_pool_with_errors.hpp`, `pthread_wrapper.hpp`) + +### Tests + +- **New regression coverage for modern callable paths** -- tests now cover + move-only `post` tasks, move-only scheduled tasks, move-only + `FutureWithErrorHandler::on_error(...)` callbacks, `PoolWithErrors` with + move-only arguments, and `ThreadInfo(Tid)` invalid-target behavior. + (`thread_pool_v2_test.cpp`, `futures_test.cpp`, `thread_config_test.cpp`) + +- **New callable benchmark target** -- `callable_benchmarks` compares small + capture, large capture, and move-only capture posting overhead on + `ThreadPool` and `HighPerformancePool` as a local performance validation + tool. (`benchmarks/callable_benchmarks.cpp`, `benchmarks/CMakeLists.txt`) + +### CI / Infrastructure + +- **Added Linux C++26 coverage for GCC 16 and Clang 22** -- the main test + workflow now installs and runs additional `ubuntu-24.04` jobs for + `gcc-16`/`g++-16` and `clang-22`/`clang++-22`, extending verification of the + modern callable and C++26 code paths without replacing the existing matrix. + (`.github/workflows/tests.yml`) + ## v2.1.0 > **No API/ABI breaking changes.** All modifications are bug fixes (aligning From d82e6210712cef98cdb04c71cfdc45c6257e7e5b Mon Sep 17 00:00:00 2001 From: Katze719 Date: Fri, 29 May 2026 20:21:10 +0200 Subject: [PATCH 5/5] docs(readme): add v2.2 feature and CI updates --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fa5d0eb..8f1d881 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ or with optional **shared runtime** for multi-DSO applications. with powerful features - **Non-owning Views**: Zero-overhead views to configure existing threads or find by name (Linux) +- **`ThreadInfo` Handles**: Lightweight bound thread handles for the current + thread or any known `Tid` - **Thread Naming**: Human-readable thread names for debugging - **Priority & Scheduling**: Fine-grained control over thread priorities and scheduling policies @@ -38,6 +40,9 @@ or with optional **shared runtime** for multi-DSO applications. box - no boilerplate promise types needed - **High-Performance Pools**: Work-stealing pool, `post()` / `try_post()`, and optional `LightweightPool` for fire-and-forget workloads with minimal overhead +- **Modern Callable Paths**: Newer standard libraries can use + `std::move_only_function` / `std::copyable_function` internally for lower + adaptation overhead while keeping the public API source-compatible - **Scheduled Tasks**: Run tasks at specific times, after delays, or periodically - **Error Handling**: Comprehensive exception handling with error callbacks and @@ -46,6 +51,21 @@ or with optional **shared runtime** for multi-DSO applications. - **RAII & Exception Safety**: Automatic resource management - **Multiple Integration Methods**: CMake, CPM, Conan, FetchContent +## What's new in v2.2 + +Version 2.2 focuses on **broader thread-control coverage**, **more modern +callable handling on newer standards**, and **wider C++26 CI coverage**. +Highlights: + +| Area | What changed | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`ThreadInfo`** | `ThreadInfo` can now bind a specific `Tid`, not just the current thread. Use it to query or configure name, priority, policy, and affinity for library-owned background threads or other known thread IDs. | +| **Background thread control**| `ScheduledThreadPoolT` exposes `scheduler_thread_info()` / `configure_scheduler_thread(...)`, and `ChaosController` exposes `thread_info()` / `configure_thread(...)`. | +| **Callable modernization** | Internal task/callback storage is feature-gated: move-only hot paths can use `std::move_only_function`, reusable hooks can use `std::copyable_function`, and older toolchains keep the `std::function` path. | +| **Move-only task support** | `post`/`try_post`, one-shot scheduled tasks, pthread entry trampolines, and error-handling wrappers now accept more move-only payloads cleanly on newer standard libraries. | +| **Tests & benchmarks** | New regression tests cover move-only tasks/callbacks and invalid `ThreadInfo(Tid)` targets. A new `callable_benchmarks` target compares small, large, and move-only task capture overhead. | +| **CI** | Linux C++26 coverage now includes `gcc-16` and `clang-22` in addition to the existing modern compiler jobs. | + ## What's new in v2.0 Version 2.0 focuses on **lower-overhead submission**, **more control over @@ -111,10 +131,12 @@ on: | Ubuntu 24.04 | GCC 13 | yes | yes | yes | - | | Ubuntu 24.04 | GCC 14 | yes | yes | yes | yes | | Ubuntu 24.04 | GCC 15 | - | yes | yes | yes | +| Ubuntu 24.04 | GCC 16 | - | - | - | yes | | Ubuntu 24.04 | Clang 16 | yes | yes | - | - | | Ubuntu 24.04 | Clang 18 | yes | yes | - | - | | Ubuntu 24.04 | Clang 19 | - | yes | yes | yes | | Ubuntu 24.04 | Clang 21 | - | yes | yes | yes | +| Ubuntu 24.04 | Clang 22 | - | - | - | yes | | **Linux (ARM64)** | | | | | | | Ubuntu 24.04 ARM64 | GCC 13 (system) | yes | yes | yes | - | | Ubuntu 24.04 ARM64 | GCC 14 | - | yes | yes | yes | @@ -138,9 +160,14 @@ are not regularly tested in CI. > > **GCC 15**: Installed via `ppa:ubuntu-toolchain-r/test` on Ubuntu 24.04. > +> **GCC 16**: Installed via `ppa:ubuntu-toolchain-r/test` on Ubuntu 24.04. +> > **Clang 21**: Installed via the official LLVM apt repository (`apt.llvm.org`) > on Ubuntu 24.04. > +> **Clang 22**: Installed via the official LLVM apt repository (`apt.llvm.org`) +> on Ubuntu 24.04. +> > **Windows ARM64**: Not currently covered by GitHub-hosted runners, requires > self-hosted runner for testing. > @@ -236,6 +263,7 @@ int main() { auto handle = scheduler.schedule_periodic(std::chrono::seconds(5), []() { std::cout << "Periodic task executed!" << std::endl; }); + scheduler.configure_scheduler_thread("sched_main"); // Or use high-performance pool for frequent tasks ScheduledHighPerformancePool scheduler_hp(4); @@ -243,6 +271,17 @@ int main() { std::cout << "Frequent task!" << std::endl; }); + // Bound thread handle for library-owned threads + if (auto info = scheduler.scheduler_thread_info()) { + (void)info->set_priority(ThreadPriority::normal()); + } + + // Move-only payloads on modern standard libraries + auto payload = std::make_unique(7); + pool.post([value = std::move(payload)]() mutable { + std::cout << "Move-only payload: " << *value << std::endl; + }); + // v2: ScheduledLightweightPool - same API, LightweightPool backend (post-based dispatch) // Error handling @@ -306,6 +345,25 @@ jv.request_stop(); jv.join(); ``` +### `ThreadInfo` for Bound Thread IDs + +Use `ThreadInfo` when you already know a `Tid` and want a lightweight control +handle without wrapping ownership. + +```cpp +#include +using namespace threadschedule; + +ScheduledThreadPool scheduler(2); + +if (auto info = scheduler.scheduler_thread_info()) { + auto tid = info->thread_id(); + ThreadInfo bound(tid); + (void)bound.set_name("scheduler_main"); + auto current_name = bound.get_name(); +} +``` + ### Global Thread Registry Opt-in registered threads with process-wide control, without imposing overhead @@ -494,7 +552,9 @@ Zero-overhead helpers to operate on existing threads without taking ownership. All of the above support `shutdown(ShutdownPolicy)` and `shutdown_for(timeout)` where applicable. Use **`post()`** when you do not need a `std::future` (lower -overhead than `submit()`). +overhead than `submit()`). On newer standard libraries, internal queueing and +hook/error-callback storage can transparently use standard move-only/copyable +call wrappers. ### Configuration