From 18b6d63a82b2d980ccd7c3eb39f4437d06e34fc9 Mon Sep 17 00:00:00 2001 From: Guillaume Riou Date: Wed, 8 Oct 2025 14:50:49 -0400 Subject: [PATCH 1/3] Improve: Add `worker_yield_t` hook for idle backoff Worker threads now invoke user-supplied `code worker_yield_t::operator()(index_type)` between jobs so real-time audio workloads can tier `code micro_yield()` before falling back to sleeps. --- include/fork_union.hpp | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/include/fork_union.hpp b/include/fork_union.hpp index 1597e3c..6cc52fc 100644 --- a/include/fork_union.hpp +++ b/include/fork_union.hpp @@ -299,6 +299,17 @@ struct standard_yield_t { inline void operator()() const noexcept { std::this_thread::yield(); } }; +/** + * @brief Yield function called by worker threads in between tasks. + * + * This implementation does not use the thread index but a user could use + * this to implement more complex yield behaviours. + */ +template +struct standard_worker_yield_t { + inline void operator()(thread_index_t_ idx) const noexcept { std::this_thread::yield(); } +}; + /** * @brief A synchronization point that waits for all threads to finish the last fork. * @note You don't have to explicitly call any of the APIs, it's like `std::jthread` ;) @@ -1007,11 +1018,12 @@ constexpr bool can_be_for_slice_callback() noexcept { * @tparam index_type_ Use `std::size_t`, but or a smaller type for debugging. * @tparam alignment_ The alignment of the thread pool. Defaults to `default_alignment_k`. */ -template < // - typename allocator_type_ = std::allocator, // - typename micro_yield_type_ = standard_yield_t, // - typename index_type_ = std::size_t, // - std::size_t alignment_ = default_alignment_k // +template < // + typename allocator_type_ = std::allocator, // + typename micro_yield_type_ = standard_yield_t, // + typename index_type_ = std::size_t, // + std::size_t alignment_ = default_alignment_k, // + typename worker_yield_type = standard_worker_yield_t // > class basic_pool { @@ -1025,6 +1037,9 @@ class basic_pool { using index_t = index_type_; static_assert(std::is_unsigned::value, "Index type must be an unsigned integer"); + using worker_yield_t = worker_yield_type; + static_assert(std::is_nothrow_invocable_r::value, + "Worker yield must be callable with a index_t argument & return void"); using epoch_index_t = index_t; // ? A.k.a. number of previous API calls in [0, UINT_MAX) using thread_index_t = index_t; // ? A.k.a. "core index" or "thread ID" in [0, threads_count) using colocation_index_t = index_t; // ? A.k.a. "NUMA node ID" in [0, numa_nodes_count) @@ -1400,10 +1415,10 @@ class basic_pool { // Wait for either: a new ticket or a stop flag epoch_index_t new_epoch; // Will definitely be initialized in the loop mood_t mood = mood_t::grind_k; // May not be initialized in the loop - micro_yield_t micro_yield; + worker_yield_t worker_yield; while ((new_epoch = epoch_.load(std::memory_order_acquire)) == last_epoch && (mood = mood_.load(std::memory_order_acquire)) == mood_t::grind_k) - micro_yield(); + worker_yield(thread_index); if (fu_unlikely_(mood == mood_t::die_k)) break; if (fu_unlikely_(mood == mood_t::chill_k) && (new_epoch == last_epoch)) { From 101a9553b4968c132b8ed80e51f840235229d078 Mon Sep 17 00:00:00 2001 From: Guillaume Riou Date: Wed, 8 Oct 2025 16:04:00 -0400 Subject: [PATCH 2/3] Fix: Route `standard_worker_yield` through `micro_yield()` Default worker path now delegates to the configured `code micro_yield_t` instead of hardwiring `code std::this_thread::yield()`, keeping custom wait strategies effective. --- include/fork_union.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/include/fork_union.hpp b/include/fork_union.hpp index 6cc52fc..d43abe9 100644 --- a/include/fork_union.hpp +++ b/include/fork_union.hpp @@ -305,9 +305,10 @@ struct standard_yield_t { * This implementation does not use the thread index but a user could use * this to implement more complex yield behaviours. */ -template +template struct standard_worker_yield_t { - inline void operator()(thread_index_t_ idx) const noexcept { std::this_thread::yield(); } + micro_yield_type micro_yield; + inline void operator()(FU_MAYBE_UNUSED_ thread_index_t_ idx) const noexcept { micro_yield; } }; /** @@ -1018,12 +1019,12 @@ constexpr bool can_be_for_slice_callback() noexcept { * @tparam index_type_ Use `std::size_t`, but or a smaller type for debugging. * @tparam alignment_ The alignment of the thread pool. Defaults to `default_alignment_k`. */ -template < // - typename allocator_type_ = std::allocator, // - typename micro_yield_type_ = standard_yield_t, // - typename index_type_ = std::size_t, // - std::size_t alignment_ = default_alignment_k, // - typename worker_yield_type = standard_worker_yield_t // +template < // + typename allocator_type_ = std::allocator, // + typename micro_yield_type_ = standard_yield_t, // + typename index_type_ = std::size_t, // + std::size_t alignment_ = default_alignment_k, // + typename worker_yield_type = standard_worker_yield_t // > class basic_pool { From 53e1b5d43de65230b263d4f3083e39ff118fac54 Mon Sep 17 00:00:00 2001 From: Guillaume Riou Date: Thu, 9 Oct 2025 09:21:42 -0400 Subject: [PATCH 3/3] Improve: Select worker-yield overloads with `if constexpr` Drops the extra template parameter by using `code if constexpr` so worker threads hand their index to `code worker_yield_t` only when that overload exists. --- include/fork_union.hpp | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/include/fork_union.hpp b/include/fork_union.hpp index d43abe9..5e4ebf8 100644 --- a/include/fork_union.hpp +++ b/include/fork_union.hpp @@ -299,18 +299,6 @@ struct standard_yield_t { inline void operator()() const noexcept { std::this_thread::yield(); } }; -/** - * @brief Yield function called by worker threads in between tasks. - * - * This implementation does not use the thread index but a user could use - * this to implement more complex yield behaviours. - */ -template -struct standard_worker_yield_t { - micro_yield_type micro_yield; - inline void operator()(FU_MAYBE_UNUSED_ thread_index_t_ idx) const noexcept { micro_yield; } -}; - /** * @brief A synchronization point that waits for all threads to finish the last fork. * @note You don't have to explicitly call any of the APIs, it's like `std::jthread` ;) @@ -1015,16 +1003,16 @@ constexpr bool can_be_for_slice_callback() noexcept { * ------------------------------------------------------------------------------------------------ * * @tparam allocator_type_ The type of the allocator to be used for the thread pool. - * @tparam micro_yield_type_ The type of the yield function to be used for busy-waiting. + * @tparam micro_yield_type_ The type of the yield function to be used for busy-waiting. If an overload of + * operator()(index_type_ thread_index) exists, worker threads will call it with their thread index. * @tparam index_type_ Use `std::size_t`, but or a smaller type for debugging. * @tparam alignment_ The alignment of the thread pool. Defaults to `default_alignment_k`. */ -template < // - typename allocator_type_ = std::allocator, // - typename micro_yield_type_ = standard_yield_t, // - typename index_type_ = std::size_t, // - std::size_t alignment_ = default_alignment_k, // - typename worker_yield_type = standard_worker_yield_t // +template < // + typename allocator_type_ = std::allocator, // + typename micro_yield_type_ = standard_yield_t, // + typename index_type_ = std::size_t, // + std::size_t alignment_ = default_alignment_k // > class basic_pool { @@ -1038,9 +1026,6 @@ class basic_pool { using index_t = index_type_; static_assert(std::is_unsigned::value, "Index type must be an unsigned integer"); - using worker_yield_t = worker_yield_type; - static_assert(std::is_nothrow_invocable_r::value, - "Worker yield must be callable with a index_t argument & return void"); using epoch_index_t = index_t; // ? A.k.a. number of previous API calls in [0, UINT_MAX) using thread_index_t = index_t; // ? A.k.a. "core index" or "thread ID" in [0, threads_count) using colocation_index_t = index_t; // ? A.k.a. "NUMA node ID" in [0, numa_nodes_count) @@ -1416,10 +1401,15 @@ class basic_pool { // Wait for either: a new ticket or a stop flag epoch_index_t new_epoch; // Will definitely be initialized in the loop mood_t mood = mood_t::grind_k; // May not be initialized in the loop - worker_yield_t worker_yield; + micro_yield_t micro_yield; while ((new_epoch = epoch_.load(std::memory_order_acquire)) == last_epoch && - (mood = mood_.load(std::memory_order_acquire)) == mood_t::grind_k) - worker_yield(thread_index); + (mood = mood_.load(std::memory_order_acquire)) == mood_t::grind_k) { + // Call micro_yield with the current thread's index if possible. + if constexpr (std::is_nothrow_invocable_r::value) + micro_yield(thread_index); + else + micro_yield(); + } if (fu_unlikely_(mood == mood_t::die_k)) break; if (fu_unlikely_(mood == mood_t::chill_k) && (new_epoch == last_epoch)) {