/
Ant010ff
/
ffpp
Обзор
Документация
Войти
/
Ant010ff
/
ffpp
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
include/functionalqueuepool.hpp
2 359 строк
90 KB
Ant010ff
Version 2.11.3.
08 авг 2026, 16:44
08 авг 2026, 16:44
fd187d3
Код
Авторство
О чём код?
/* This header is part of a Functional Flow Processing Primitives (FFPP) library, version 2.11.3. Official repository: https://gitlab.com/ant010ff/ffpp Licensed under the MIT License <http://opensource.org/licenses/MIT>. SPDX-License-Identifier: MIT Copyright (c) 2021 - 2026 Anton Nasonov <ant010fff @ gmail . com>. */ #ifndef FFPP_FUNCTIONAL_QUEUE_POOL_HPP #define FFPP_FUNCTIONAL_QUEUE_POOL_HPP namespace FFPP { struct PoolMetrics { size_t nActiveThreads //immediate number of active threads, including dispatcher (on the moment of metrics request) , nBusyThreads //immediate number of busy threads , nDispatchQueueSize //immediate number of pending tasks in dispatcher's queue , nPendingTasks //total number of pending tasks , nContention //pool's contention score as a sum of contention scores of active processing threads , nProcessed //number of tasks processed (since last metrics request) , nIdleDequeues //number of tasks dispatched via idle-queue (since last metrics request) , nIdleFailures //number of idle-queue failures (since last metrics request) , nOffloads //number of tasks offloaded (since last metrics request) , nSteals //number of tasks stolen (since last metrics request) , nFailedSteals //number of failed task steal attempts (since last metrics request) , nReshuffles //number of randomized topology reshuffles (since last metrics request) ; }; using PoolMetricsHandlerT = Function<void(PoolMetrics const&)>; struct PoolPolicy : ThreadPolicy { //Static thread pool (ffpp::DispatchFlags::Static specified) do not require pool-wide locking. In contrast dynamic pool //needs the lock to synchronize direct task dispatch with pool maintenance. But maintenance usually is a rare operation //with period of 30 seconds by default. While direct dispatch itself does not require lock and can be performed concurrently. //A StripedTicketMutex significantly mitigates dispatchers' (threads invoking dispatch logic when emitting or offloading //tasks) contention in high-load cases, bringing performance in dynamic mode close to that of a static mode. using LockableT = Lockable<StripedTicketMutex<>>; static constexpr SchedulingFlags SchedulingMethod() { return SchedulingFlags::Disabled; } static constexpr DispatchFlags DispatchMethod() { return DispatchFlags::Default; } static constexpr auto StealBackoff() { //By default performing several steal attempts with exponential series of CPU relaxation loops before going to wait //state, with factors [4, 6] (from 16 to 64) with random deviation within +-25% from the exponential base. With current //values this will give 3 additional task-stealing attempts on each try (4 attempts total). return FFPP::NoisyBackoff<false, 6, 0, 0, 4, 2>(); } static size_t ThreadGroupSize(size_t nThreads) noexcept { if(nThreads < 256 /*1024*/) return 16; //32 else return std::bit_ceil(math::Sqrt(nThreads)); } static bool OnPoolThreadStart() noexcept { return ThreadPolicy::OnThreadStart(); } static void OnPoolThreadStop() noexcept { ThreadPolicy::OnThreadStop(); } static bool OnPoolThreadException(std::exception const& ex) noexcept { return ThreadPolicy::OnThreadException(ex); } static bool OnDispatchThreadException(std::exception const&) noexcept { return true; } static void OnStealException(std::exception const&) noexcept { } }; };//FFPP namespace FFPP::Concepts { template<typename Type> concept PoolPolicy = ThreadPolicy<Type> && ConstexprInvocableStrict<Type::DispatchMethod, DispatchFlags> && Invocable<Type::StealBackoff, BackoffHandlerT> //task steal backoff handler generator && InvocableStrict<Type::ThreadGroupSize, size_t, size_t> && InvocableStrict<Type::OnPoolThreadStart, bool> && Invocable<Type::OnPoolThreadStop, void> && InvocableStrict<Type::OnPoolThreadException, bool, std::exception> && InvocableStrict<Type::OnDispatchThreadException, bool, std::exception> && Invocable<Type::OnStealException, void, std::exception> ;//PoolPolicy }//FFPP::Concepts namespace FFPP { template<Concepts::PoolPolicy TPolicy = PoolPolicy> class FunctionalQueuePool : public std::enable_shared_from_this<FunctionalQueuePool<TPolicy>> { public: struct Policy : TPolicy { using ResourcePolicyT = TPolicy::ResourcePolicyT; static constexpr size_t c_nSparseDimension = 6; //log(log(2^64)), though even 5 looks unrealistic, 4 -> 2 ^ 16 threads using SparseEdgesT = std::array<size_t, c_nSparseDimension>; static bool SetThreadAffinity(ThreadHandleT hThread, size_t iThread) { if constexpr(requires { TPolicy::SetThreadAffinity(ThreadHandleT { }, size_t { }); }) { return TPolicy::SetThreadAffinity(hThread, iThread); } else { return ResourcePolicyT::SetThreadAffinity(hThread, iThread); } } FFPP_ATTR_INLINE static size_t Random() noexcept { return prng::XoShiRo(); } };//Policy using PolicyT = Policy; using ResourcePolicyT = PolicyT::ResourcePolicyT; using FunctionalQueuePoolT = FunctionalQueuePool<PolicyT>; template<typename TSignature, bool t_bUnique = true> using FunctionT = ResourcePolicyT::template FunctionT<TSignature, t_bUnique>; FunctionalQueuePool( size_t nPoolLimit = std::max<uint32_t>(std::thread::hardware_concurrency(), 2ul) , std::chrono::seconds secIdleTimeout = std::chrono::seconds(30) #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin = std::source_location::current() #endif ) : c_sftDispatcher_(MakeDispatcher( nPoolLimit , secIdleTimeout #if FFPP_TRACK_ORIGIN , slOrigin #endif )) {} ~FunctionalQueuePool() { Complete(Flags::Wait); } FunctionalQueuePool(FunctionalQueuePool const&) = delete; FunctionalQueuePool(FunctionalQueuePool&&) = delete; FunctionalQueuePool& operator = (FunctionalQueuePool const&) = delete; FunctionalQueuePool& operator = (FunctionalQueuePool&&) = delete; FFPP_ATTR_INLINE static ResourcePolicyT::template SharedT<FunctionalQueuePoolT> MakeShared( size_t nPoolLimit = std::max<uint32_t>(std::thread::hardware_concurrency(), 2ul) , std::chrono::seconds secIdleTimeout = std::chrono::seconds(30) #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin = std::source_location::current() #endif ) { return ResourcePolicyT::template AllocateShared<FunctionalQueuePoolT>( nPoolLimit , secIdleTimeout #if FFPP_TRACK_ORIGIN , slOrigin #endif ); } FFPP_ATTR_INLINE IdProvider::Id GetInstanceId() const noexcept { return c_sftDispatcher_->GetInstanceId(); } template<Concepts::Executable TExecutable> FFPP_ATTR_HOT_PATH bool Submit(typename ResourcePolicyT::template SharedT<TExecutable> const& spExec) { if constexpr(!Dispatcher::template IsScheduling<TExecutable>(SchedulingFlags::Concurrent)) { BindExecutor(spExec); } return c_sftDispatcher_->Submit(spExec); } template<std::convertible_to<PoolMetricsHandlerT> THandler> bool ForPoolMetrics(THandler&& onPoolMetrics, uint32_t nPriority = 0) { return c_sftDispatcher_->Enqueue(PoolMetricsHandlerT { std::forward<THandler>(onPoolMetrics) }, nPriority); } bool Finalize(Flags flgContext = Flags::Undefined) const noexcept { return c_sftDispatcher_->Finalize(flgContext); } template<std::invocable TFunctional> bool Complete(Flags flgContext, TFunctional&& onComplete) { return c_sftDispatcher_->Complete(flgContext, std::forward<TFunctional>(onComplete)); } template<std::invocable TFunctional> bool Complete(TFunctional&& onComplete) { return Complete(Flags::Undefined, std::forward<TFunctional>(onComplete)); } bool Complete(Flags flgContext = Flags::Wait) { return Complete(flgContext, FunctionT<void()>{ }); } bool Wait(Flags flgContext = Flags::Undefined) const noexcept { return c_sftDispatcher_->Wait(flgContext); } size_t GetThreadsLimit() const noexcept { return c_sftDispatcher_->GetThreadsLimit(); } static size_t CalcThreadsLimit(size_t nLimit) noexcept { if constexpr(!(DispatchFlags::Topology & PolicyT::DispatchMethod())) return Dispatcher::CalcThreadsLimit(nLimit); else return Dispatcher::CalcThreadsLimit(std::max(size_t(2), nLimit)); } static consteval bool IsDynamic() { return (PolicyT::DispatchMethod() & DispatchFlags::Static) == DispatchFlags::Undefined; } protected: friend class Factory<FunctionalQueuePool<TPolicy>>; template<bool t_bInternalInvocation = false> void SetOrigin(std::source_location const& slOrigin) noexcept { c_sftDispatcher_->template SetOrigin<t_bInternalInvocation>(slOrigin); } template<Concepts::Executable TExecutable> FFPP_ATTR_INLINE void BindExecutor(typename ResourcePolicyT::template SharedT<TExecutable> const& spExec) const { auto& idExecutor = spExec->template GetExecutorId<true>(); auto idBound = idExecutor.load(std::memory_order::relaxed); auto const idDispatcher = c_sftDispatcher_->GetInstanceId(); if(idBound != idDispatcher) [[unlikely]] { if(idBound == TExecutable::InvalidValue()) [[likely]] { bool const bBound = idExecutor.compare_exchange_strong( idBound , idDispatcher , std::memory_order::relaxed , std::memory_order::relaxed ) || idBound == idDispatcher; if(bBound) [[likely]] { if constexpr( Dispatcher::IsDispatch(DispatchFlags::Locality) && Dispatcher::template IsScheduling<TExecutable>(SchedulingFlags::Deferred) && !Dispatcher::EqDispatch(DispatchFlags::Single) && (!Dispatcher::IsDispatch(DispatchFlags::Static) || Dispatcher::IsDispatch(DispatchFlags::Indirect)) ) { //This internal id is just an efficiency hint, and should not impact correctness in case of a race. size_t const idThread = c_sftDispatcher_->GetCurrentTopologyId(); if(idThread != Dispatcher::InvalidTopologyId()) { auto& idProcessor = spExec->template GetProcessorId<true>(); idProcessor.store(idThread, std::memory_order::relaxed); } } return; } } Except<>::Throw( "Executable instance already bound to another executor @" #if FFPP_TRACK_ORIGIN , c_sftDispatcher_->GetOrigin() , spExec->GetOrigin() #endif ); } } private: struct DispatcherThreadPolicy : PolicyT { static bool OnThreadStart() noexcept { if constexpr((DispatchFlags::Affinity & PolicyT::DispatchMethod()) != DispatchFlags::Undefined) { size_t const nConcurrency = std::thread::hardware_concurrency(); if(nConcurrency > 1) PolicyT::SetThreadAffinity( FFPP::GetCurrentNativeThread(), PolicyT::Random() % nConcurrency ); } return PolicyT::OnPoolThreadStart(); } static void OnThreadStop() noexcept { PolicyT::OnPoolThreadStop(); } static bool OnThreadException(std::exception const& ex) noexcept { return PolicyT::OnDispatchThreadException(ex); } }; class Dispatcher : public FunctionalQueueThread<Dispatcher, DispatcherThreadPolicy, IsDynamic()> { class ProcessingThread; public: using PolicyT = DispatcherThreadPolicy; using ResourcePolicyT = PolicyT::ResourcePolicyT; using LockableT = PolicyT::LockableT; using UniqueLockT = LockableT::UniqueLockT; using SharedLockT = LockableT::SharedLockT; using FunctionalQueueThreadT = FunctionalQueueThread<Dispatcher, PolicyT, IsDynamic()>; using ClockT = FunctionalQueueThreadT::ClockT; friend FunctionalQueueThreadT; using FunctionalSequenceT = FunctionalQueueThreadT::FunctionalSequenceT; friend FunctionalSequenceT; using ExecutableT = FunctionalSequenceT::ExecutableT; friend ExecutableT; using FunctionalQueueT = FunctionalQueueThreadT::FunctionalQueueT; friend FunctionalQueueT; Dispatcher( size_t nThreadsLimit , std::chrono::milliseconds msTimingPeriod , std::chrono::milliseconds msIdleTimeout #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin #endif ) : FunctionalQueueThreadT( msTimingPeriod , FunctionalSequenceT::InvalidLimit() #if FFPP_TRACK_ORIGIN , slOrigin #endif ) , c_nThreadsLimit_(CalcThreadsLimit(nThreadsLimit)) , c_nThreadGroup_(CalcThreadGroupSize(CalcThreadsLimit(nThreadsLimit))) , c_nLogThreads_(CalcLogThreads(nThreadsLimit)) , c_nLogLogThreads_(CalcLogLogThreads(nThreadsLimit)) , c_msIdleTimeout_(msIdleTimeout) , c_fmodThreads_(CalcThreadsLimit(nThreadsLimit)) , c_mskLogThreads_((size_t(1) << CalcLogThreads(nThreadsLimit)) - 1) , c_mskLogLogThreads_((size_t(1) << CalcLogLogThreads(nThreadsLimit)) - 1) , c_mskDefaultTopology_(MakeTopologyMask(GetTopologyMaskParams(nThreadsLimit))) , c_nGroupShift_(CalcGroupShift(CalcThreadsLimit(nThreadsLimit))) , c_nIdleQueues_(CalcIdleQueueSegments(CalcThreadsLimit(nThreadsLimit)).second) , c_vIdleQueues_(MakeIdleQueueVector(CalcThreadsLimit(nThreadsLimit))) , nSuspended_(IsDispatch(DispatchFlags::Static) ? 0 : CalcThreadsLimit(nThreadsLimit)) , vThreads_(MakeThreadVector( this , CalcThreadsLimit(nThreadsLimit) #if FFPP_TRACK_ORIGIN , slOrigin #endif )) { static_assert( !(IsDispatch(DispatchFlags::Steal) && IsDispatch(DispatchFlags::Bind, true)) || EqDispatch(DispatchFlags::Single), "Task-stealing is incompatible with task to thread binding." ); if constexpr(c_bIdleQueue_ && IsDispatch(DispatchFlags::Static)) { for(size_t iThread = 0; iThread < c_nThreadsLimit_; ++iThread) { EnqueueIdleThread(iThread); } } if constexpr(IsDynamic()) { this->CommitThread(); } } ~Dispatcher() { } static size_t CalcThreadsLimit(size_t nLimit) noexcept { if constexpr(IsDispatch(DispatchFlags::Lower2, true)) return std::bit_floor(nLimit); else if constexpr(IsDispatch(DispatchFlags::Align2)) return std::bit_ceil(nLimit); else return nLimit; } FFPP_ATTR_INLINE IdProvider::Id GetInstanceId() const noexcept { return c_idInstance_; } static constexpr size_t InvalidTopologyId() noexcept { return std::numeric_limits<size_t>::max(); } FFPP_ATTR_INLINE size_t GetCurrentTopologyId() noexcept { auto* pptCurrent = GetCurrentProcessingThread(); if(pptCurrent == nullptr) return InvalidTopologyId(); return pptCurrent->GetTopologyId(); } template<bool t_bInternalInvocation = false> void SetOrigin(std::source_location const& slOrigin) noexcept { this->FunctionalSequenceT::template SetOrigin<t_bInternalInvocation>(slOrigin); if constexpr(IsDispatch(DispatchFlags::Static)) { for(auto const& spThread : vThreads_) { spThread->template SetOrigin<t_bInternalInvocation>(slOrigin); } } } template<Concepts::Executable TExecutable> FFPP_ATTR_HOT_PATH bool Submit(typename ResourcePolicyT::template SharedT<TExecutable> const& spExec) { static_assert( EqDispatch(DispatchFlags::Single) || !IsDispatch(DispatchFlags::Steal) || IsScheduling<TExecutable>(SchedulingFlags::Deferred) || IsScheduling<TExecutable>(SchedulingFlags::Concurrent) , "Task-stealing requires deferred or concurrent scheduling." ); static_assert( EqDispatch(DispatchFlags::Single) || !IsDispatch(DispatchFlags::Steal) || !BindingEnabled<TExecutable>() , "Task-stealing is not compatible with task to thread binding." ); static_assert( EqDispatch(DispatchFlags::Single) || !OffloadingEnabled<TExecutable>() || !BindingEnabled<TExecutable>() , "Offloading is not compatible with task to thread binding." ); if constexpr(EqDispatch(DispatchFlags::Single)) { //Dispatcher is used as a single processing thread. return DispatchSingle(spExec); } else if constexpr(IsDispatch(DispatchFlags::Indirect)) { //Indirect synchronized dispatch via dispatcher's queue. return DispatchIndirect(spExec); } else if constexpr(IsDispatch(DispatchFlags::Static)) { //Direct concurrent dispatch in context of a calling thread. Requires no additional synchronization because //all processing threads were started on pool's initialization and will never be destroyed during pool lifetime. //When idle they just go to wait state. return DispatchDirect(spExec); } else { //In direct dispatch mode (DispatchFlags::Indirect not specified) with dynamic thread pool task submits //must be performed mutually exclusive with maintenance (in context of dispatcher's thread, OnThreadTiming) //but not with each other. auto sl = this->GetSharedLock(); //When there are suspended processing threads (pool was not active - just started or activated after idle //period) direct dispatch is not possible because there are null thread objects in pool's vector. if(nSuspended_.load(std::memory_order::relaxed) > 0) { sl.unlock(); return DispatchIndirect(spExec); } else { //When load goes higher and pool's processing threads limit reached a direct dispatch becomes available. return DispatchDirect(spExec); } } } bool Finalize(Flags flgContext) noexcept { if constexpr(!EqDispatch(DispatchFlags::Single)) { //Direct pool finalization requires dispatcher thread to be finalized (and joined) first because it is the //only one allowed to modify processing thread vector vThreads_ (with no locks). Thus waiting on dispatcher //is necessary. If this is unacceptable a Complete method should be used (alows to implement complex wait //strategies or schedule waitless completion in context of pool itself). bool bFinalize = FunctionalQueueThreadT::Finalize(flgContext & ~Flags::Wait); //do not wait inside //Now it is required to wait for dispatcher thread itself, so using here base class' wait implementation. if(!this->IsProcessingThread()) bFinalize = FunctionalQueueThreadT::OnWait(flgContext) && bFinalize; //Finally it is able to finalize and wait for processing threads one-by-one using vThreads_ directly. for(auto const& spThread : vThreads_) { if(!spThread) continue; if(spThread->IsProcessingThread()) bFinalize = spThread->Finalize(flgContext & ~Flags::Wait) && bFinalize; else bFinalize = spThread->Finalize(flgContext | Flags::Wait) && bFinalize; } return bFinalize; } else { //In a single thread pool dispatcher thread itself is used as processing thread and can be finalized in any way. return FunctionalQueueThreadT::Finalize(flgContext); } } template<std::invocable TCompleteHandler> bool Complete(Flags flgContext, TCompleteHandler&& onComplete) { return FunctionalQueueThreadT::Complete( flgContext, [flgContext, onComplete = FunctionT<void()> { std::move(onComplete) }] (auto* pSelf) mutable { if constexpr(!EqDispatch(DispatchFlags::Single)) { //This handler is invoked sequentially in context of dispatcher thread which is the only one allowed //to modify processing thread vector vThreads_. So no additional thread synchronization required here. if(!pSelf->vThreads_.empty()) { if(!!(flgContext & Flags::Wait)) { auto [_, sgComplete] = BeginJoin(); for(auto const& spThread : pSelf->vThreads_) { if(spThread) spThread->Complete(flgContext & ~Flags::Wait, [sgComplete] (auto...) { }); } } else { for(auto const& spThread : pSelf->vThreads_) { if(spThread) spThread->Complete(flgContext); } } } } if(onComplete) std::invoke(onComplete); } ); } FFPP_ATTR_INLINE size_t GetThreadsLimit() const noexcept { return c_nThreadsLimit_; } static consteval bool IsDispatch(DispatchFlags flg, bool bAll = false) { if(bAll) return (flg & PolicyT::DispatchMethod()) == flg; else return (flg & PolicyT::DispatchMethod()) != DispatchFlags::Undefined; } static consteval bool EqDispatch(DispatchFlags flg) { return PolicyT::DispatchMethod() == flg; } template<Concepts::Executable TExecutable> static consteval bool IsScheduling(SchedulingFlags flg, bool bAll = false) { if(bAll) return (flg & TExecutable::PolicyT::SchedulingMethod()) == flg; else return (flg & TExecutable::PolicyT::SchedulingMethod()) != SchedulingFlags::Undefined; } template<Concepts::Executable TExecutable> static consteval bool EqScheduling(SchedulingFlags flg) { return TExecutable::PolicyT::SchedulingMethod() == flg; } template<Concepts::Executable TExecutable> static consteval bool OffloadingEnabled() { return !IsDispatch(DispatchFlags::Bind, true) && !IsDispatch(DispatchFlags::Locality, true) && ( IsScheduling<TExecutable>(SchedulingFlags::Offload, true) || IsScheduling<TExecutable>(SchedulingFlags::Concurrent) ) ; } template<Concepts::Executable TExecutable> static consteval bool BindingEnabled() { if constexpr(IsDispatch(DispatchFlags::Bind, true)) { return true; } else if constexpr(IsScheduling<TExecutable>(SchedulingFlags::Bind)) { static_assert(IsDispatch(DispatchFlags::Static), "Task to thread binding requires static thread pool."); return true; } return false; } protected: FFPP_ATTR_INLINE bool OffloadAvailable() const noexcept { return GetBusyCount() < c_nThreadsLimit_; } FFPP_ATTR_HOT_PATH bool Steal(ProcessingThread* pptThief) { FFPP_TRY { if(c_nThreadsLimit_ < 2) [[unlikely]] return false; bool constexpr c_bLinear = !IsDispatch(DispatchFlags::Topology | DispatchFlags::Randomized), c_bResteal = IsDispatch(DispatchFlags::Resteal, true), c_bWideSteal = IsDispatch(DispatchFlags::WideSteal, true) && !c_bLinear; typename PolicyT::LockableT::SharedLockT sl; if constexpr(!IsDispatch(DispatchFlags::Static)) sl = this->GetSharedLock(); typename ProcessingThread::FunctionalT fnTask; uint32_t idqTask = ProcessingThread::InvalidQueueId(); auto onBackoff = PolicyT::StealBackoff(); do { if constexpr(c_bLinear) { TryStealLinear(pptThief, idqTask, fnTask); } else if constexpr(IsDispatch(DispatchFlags::Sparse | DispatchFlags::Hypercube, true)) { TryStealSparseHypercube(pptThief, idqTask, fnTask); } else if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { TryStealSparse(pptThief, idqTask, fnTask); } else if constexpr(IsDispatch(DispatchFlags::Hypercube, true)) { TryStealHypercube(pptThief, idqTask, fnTask); } else if constexpr(IsDispatch(DispatchFlags::RandomL2, true)) { TryStealRandomL2(pptThief, idqTask, fnTask); } else if constexpr(IsDispatch(DispatchFlags::Random2, true)) { TryStealRandom2(pptThief, idqTask, fnTask); } else if constexpr(IsDispatch(DispatchFlags::Random, true)) { TryStealRandom(pptThief, idqTask, fnTask); } else { static_assert(std::is_void_v<ProcessingThread>, "Invalid task steal method!"); } //Break immediately and proceed to enqueue if task was stolen. if(idqTask != ProcessingThread::InvalidQueueId()) break; //Steal attempt failed - check whether pending tasks appeared in pptThief queues and abort task-stealing if //one of internal sub-queues not empty (relaxed atomic counter read). if(pptThief->Size() != 0) return false; } while(c_bResteal && onBackoff()); if constexpr(c_bWideSteal) { //Perform a single pool-wide task-stealing attempt if constrained steal failed. if(idqTask == ProcessingThread::InvalidQueueId()) { TryStealLinear(pptThief, idqTask, fnTask); } } if(idqTask != ProcessingThread::InvalidQueueId()) { //Task was successfully stolen and should be enqueued into pptThief along with other tasks if any happened to //appear to the current moment. Inplace immediate processing could violate task interleaving logic and stuck //pptThief upper processing loop. if(pptThief->Enqueue(std::move(fnTask), idqTask)) { pptThief->template OnTaskSteal<true>(); } else if(!pptThief->IsComplete()) { Except<>::Throw( "Failed to enqueue task into stealing thread @" #if FFPP_TRACK_ORIGIN , this->GetOrigin() #endif ); } return true; } return false; } FFPP_CATCH(std::exception, ex) { if constexpr(requires { PolicyT::OnStealException(ex); }) { PolicyT::OnStealException(ex); } return false; } } private: using CounterT = size_t; using AtomicCounterT = std::atomic<CounterT>; struct ProcessingThreadPolicy : PolicyT { using DispatcherThreadPolicyT = PolicyT; static bool OnThreadStart() noexcept { return PolicyT::OnPoolThreadStart(); } static void OnThreadStop() noexcept { PolicyT::OnPoolThreadStop(); } static bool OnThreadException(std::exception const& ex) noexcept { return PolicyT::OnPoolThreadException(ex); } static constexpr auto IdleBackoff() { if constexpr(IsDispatch(DispatchFlags::Steal)) { //Back-off for processing threads will be applied during task-stealing attempts loop, see StealBackoff(). //Dispatcher will use idle back-off specified in policy. return FFPP::NoBackoff(); } else { //Using policy defined idle back-off generator when task-stealing disabled (as for Dispatcher). return PolicyT::IdleBackoff(); } } }; struct MsgCheckIdleThreads { ClockT::time_point tpTiming; size_t iStart = 0, iStop = 0; }; struct alignas(ResourcePolicyT::InterferenceSize()) ThreadLocalState { IdProvider::Id idDispatcher = IdProvider::InvalidId(); ProcessingThread* pptCurrent = nullptr; }; #if (defined(_MSC_VER) && _MSC_VER < 1930L) static inline __declspec(thread) ThreadLocalState l_tls_ { }; #else static inline thread_local constinit ThreadLocalState l_tls_ { }; #endif class ProcessingThread : public FFPP::FunctionalQueueThread<ProcessingThread, ProcessingThreadPolicy, false> { public: friend Dispatcher; using PolicyT = ProcessingThreadPolicy; using ResourcePolicyT = PolicyT::ResourcePolicyT; using FunctionalQueueThreadT = FunctionalQueueThread<ProcessingThread, PolicyT, false>; using FunctionalSequenceT = FunctionalQueueThreadT::FunctionalSequenceT; friend FunctionalSequenceT; using ExecutableT = FunctionalSequenceT::ExecutableT; friend ExecutableT; using FunctionalQueueT = FunctionalQueueThreadT::FunctionalQueueT; friend FunctionalQueueT; using ClockT = Dispatcher::ClockT; using DurationT = std::chrono::milliseconds; using FunctionalT = FunctionalSequenceT::FunctionalT; using SparseEdgesT = PolicyT::SparseEdgesT; static constexpr size_t c_nSparseDimension = PolicyT::c_nSparseDimension, c_cbAlignment = ResourcePolicyT::InterferenceSize(); ProcessingThread( Dispatcher* pDispatcher , size_t idNode , size_t mskTopology , SparseEdgesT&& aEdges #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin #endif ) : FunctionalQueueThreadT( [idDispatcher = pDispatcher->GetInstanceId(), idNode, this] () { ThreadLocalState& tls = l_tls_; tls.idDispatcher = idDispatcher; tls.pptCurrent = this; if constexpr(IsDispatch(DispatchFlags::Affinity)) { size_t const nConcurrency = std::thread::hardware_concurrency(); if(nConcurrency > 1) PolicyT::SetThreadAffinity(FFPP::GetCurrentNativeThread(), idNode); } } , FunctionalSequenceT::InvalidLimit() #if FFPP_TRACK_ORIGIN , slOrigin #endif ) , c_pDispatcher_(pDispatcher) , c_idNode_(idNode) , mskTopology_(mskTopology) , aSparseEdges_(std::move(aEdges)) { } ~ProcessingThread() { } FFPP_ATTR_INLINE size_t GetTopologyId() const noexcept { return c_idNode_; } FFPP_ATTR_INLINE size_t GetTopologyMask() const noexcept { assert(this->IsProcessingThread()); return mskTopology_; } FFPP_ATTR_INLINE void UpdateTopologyMask(size_t mskTopology) noexcept { assert(this->IsProcessingThread()); mskTopology_ = mskTopology; nReshuffles_.fetch_add(1, std::memory_order::relaxed); } FFPP_ATTR_INLINE SparseEdgesT const& GetSparseEdges() const noexcept { return aSparseEdges_; } FFPP_ATTR_INLINE size_t GetSparseNode(size_t mskEdge) const { assert(this->IsProcessingThread()); size_t const iEdge = std::countr_zero(mskEdge); assert(iEdge < PolicyT::c_nSparseDimension); return aSparseEdges_[iEdge]; } FFPP_ATTR_INLINE void UpdateSparseEdge(size_t iEdge, size_t idNode) { assert(this->IsProcessingThread()); assert(iEdge < PolicyT::c_nSparseDimension); aSparseEdges_[iEdge] = idNode; nReshuffles_.fetch_add(1, std::memory_order::relaxed); } FFPP_ATTR_INLINE void UpdateSparseEdges(SparseEdgesT&& aEdges) { assert(this->IsProcessingThread()); aSparseEdges_ = std::move(aEdges); nReshuffles_.fetch_add(1, std::memory_order::relaxed); } bool TestIdle(ClockT::time_point const& tpTiming, std::chrono::milliseconds const& msTimeout) { if constexpr(IsDispatch(DispatchFlags::Static)) return false; if(this->HasEnqueued()) return false; size_t const nProcessed = nProcessed_.load(std::memory_order::relaxed); if(nProcessed != nActivity_.exchange(nProcessed, std::memory_order::relaxed)) { msCheckpoint_.store(GetCompatibleTime(), std::memory_order::relaxed); return false; } using std::chrono::duration_cast; auto const nTiming = duration_cast<DurationT>(tpTiming.time_since_epoch()).count(); return nTiming - msCheckpoint_.load(std::memory_order::relaxed) > msTimeout.count(); } size_t Offloads() noexcept { size_t const nOffloads = nOffloads_.load(std::memory_order::relaxed), nCheckPoint = nOffloadsCheck_.exchange(nOffloads, std::memory_order::relaxed); return nOffloads - nCheckPoint; } size_t Processed() noexcept { size_t const nProcessed = nProcessed_.load(std::memory_order::relaxed), nCheckPoint = nProcessedCheck_.exchange(nProcessed, std::memory_order::relaxed); return nProcessed - nCheckPoint; } FFPP_ATTR_INLINE size_t Reshuffles() noexcept { return nReshuffles_.exchange(0, std::memory_order::relaxed); } template<bool t_bSuccessful> size_t Steals() noexcept { if constexpr(t_bSuccessful) return nSuccessfulSteals_.exchange(0, std::memory_order::relaxed); else return nFailedSteals_.exchange(0, std::memory_order::relaxed); } template<Concepts::Executable TExecutable> FFPP_ATTR_HOT_PATH void On(typename ResourcePolicyT::template SharedT<TExecutable> const& spExec) { using TExecutablePointer = TExecutable*; auto constexpr c_flgOffloaded = AtomicCounterT::value_type(SchedulingFlags::Offload); bool constexpr c_bExceptionHandler = requires { PolicyT::DispatcherThreadPolicyT::OnProcessingException(TExecutablePointer { }, std::exception { }); }; UniqueGuard sg { [&spExec] () noexcept { if constexpr(!IsScheduling<TExecutable>(SchedulingFlags::Concurrent)) { spExec->template GetBindingCounter<true>().fetch_sub(1, std::memory_order::relaxed); } } }; if constexpr(OffloadingEnabled<TExecutable>()) { if(c_pDispatcher_->OffloadAvailable() && this->Size() > c_nOffloadThreshold_) { //Disable double offloading attempt for currently unprocessed task. AtomicCounterT& flgState = spExec->template GetStateFlags<true>(); if((flgState.fetch_or(c_flgOffloaded, std::memory_order::relaxed) & c_flgOffloaded) == 0) { sg.Finalize(); if constexpr(c_bExceptionHandler) { FFPP_TRY { if(!c_pDispatcher_->Submit(spExec)) { Except<>::Throw( "Failed to resubmit functional sequence @" #if FFPP_TRACK_ORIGIN , spExec->GetOrigin() , this->GetOrigin() #endif ); } } FFPP_CATCH(std::exception, ex) { PolicyT::DispatcherThreadPolicyT::OnProcessingException(spExec.get(), ex); } } else { if(!c_pDispatcher_->Submit(spExec)) { Except<>::Throw( "Failed to resubmit functional sequence @" #if FFPP_TRACK_ORIGIN , spExec->GetOrigin() , this->GetOrigin() #endif ); } } nOffloads_.fetch_add(1, std::memory_order::relaxed); return; } } } if constexpr(OffloadingEnabled<TExecutable>()) { //Re-enable offloading for ready to process task. AtomicCounterT& flgState = spExec->template GetStateFlags<true>(); flgState.fetch_and(~c_flgOffloaded, std::memory_order::relaxed); } if constexpr(c_bExceptionHandler) { FFPP_TRY { spExec->Process(); } FFPP_CATCH(std::exception, ex) { PolicyT::DispatcherThreadPolicyT::OnProcessingException(spExec.get(), ex); } } else { spExec->Process(); } nProcessed_.fetch_add(1, std::memory_order::relaxed); } template<bool t_bSuccess> void OnTaskSteal() noexcept { if constexpr(t_bSuccess) nSuccessfulSteals_.fetch_add(1, std::memory_order::relaxed); else nFailedSteals_.fetch_add(1, std::memory_order::relaxed); } protected: template<bool t_bNotify = true> FFPP_ATTR_HOT_PATH std::pair<bool, size_t> OnEnqueued(uint32_t idQueue) { auto [bEnqueued, nEnqueued] = FunctionalQueueThreadT::template OnEnqueued<t_bNotify>(idQueue); if(!bEnqueued) return { false, nEnqueued }; if(nEnqueued == 0) c_pDispatcher_->OnThreadEngage(); return { bEnqueued, nEnqueued }; } template<bool t_bProcessed = false> FFPP_ATTR_HOT_PATH size_t OnDequeued(uint32_t idQueue, CompatibleCounterT nDequeue = 1) noexcept(!t_bProcessed) { auto const nEnqueued = FunctionalQueueThreadT::template OnDequeued<t_bProcessed>(idQueue, nDequeue); if(nEnqueued == 1) { this->ResetContentionLevel(); c_pDispatcher_->template OnThreadDisengage<t_bProcessed>(this); } return nEnqueued; } private: static size_t constexpr //The lower queue depth required to offload current task. There is no sens to offload if there are no pending //tasks. c_nOffloadThreshold_ = 1; Dispatcher* const c_pDispatcher_ = nullptr; size_t const c_idNode_ = 0; size_t mskTopology_ = 0; std::atomic<DurationT::rep> msCheckpoint_ = 0; SparseEdgesT aSparseEdges_ alignas(c_cbAlignment); AtomicCounterT nActivity_ alignas(c_cbAlignment) = 0, nProcessed_ = 0, nProcessedCheck_ = 0, nOffloads_ alignas(c_cbAlignment) = 0, nOffloadsCheck_ = 0, nSuccessfulSteals_ alignas(c_cbAlignment) = 0, nFailedSteals_ = 0, nReshuffles_ alignas(c_cbAlignment) = 0; static uint64_t GetCompatibleTime() { return std::chrono::duration_cast<DurationT>(ClockT::now().time_since_epoch()).count(); } };//ProcessingThread using ProcessingThreadPointerT = ResourcePolicyT::template SharedT<ProcessingThread>; using ProcessingThreadVectorT = std::vector< ProcessingThreadPointerT, typename ResourcePolicyT::template AllocatorT<ProcessingThreadPointerT> >; using SparseEdgesT = ProcessingThread::SparseEdgesT; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class IdleQueue { public: struct IdleQueuePolicy : RingQueuePolicy { static constexpr auto QueueMode() { return QueueFlags::Fixed; } static size_t QueueCapacity() { return 2; } }; using IdleQueueT = RingQueue<size_t, IdleQueuePolicy>; static size_t constexpr c_cbAlignment = ResourcePolicyT::InterferenceSize(); static bool constexpr c_bNoexcept = IdleQueueT::IsNoexcept(); IdleQueue(size_t nCapacity) : qIdle_(nCapacity) { } FFPP_ATTR_INLINE void Enqueue(size_t iThread) noexcept(c_bNoexcept) { qIdle_.Enqueue(std::move(iThread)); } FFPP_ATTR_INLINE bool Dequeue(size_t& iThread) noexcept(c_bNoexcept) { bool bDequeued = qIdle_.Dequeue(iThread); if(bDequeued) nDequeues_.fetch_add(1, std::memory_order::relaxed); else nFailures_.fetch_add(1, std::memory_order::relaxed); return bDequeued; } FFPP_ATTR_INLINE void Reset() noexcept(c_bNoexcept) { size_t iThread = 0; while(qIdle_.Dequeue(iThread)); } FFPP_ATTR_INLINE size_t Dequeues() noexcept { return nDequeues_.exchange(0, std::memory_order::relaxed); } FFPP_ATTR_INLINE size_t Failures() noexcept { return nFailures_.exchange(0, std::memory_order::relaxed); } private: IdleQueueT qIdle_ alignas(c_cbAlignment); AtomicCounterT nDequeues_ alignas(c_cbAlignment) = 0, nFailures_ alignas(c_cbAlignment) = 0; };//IdleQueue using IdleQueuePointerT = ResourcePolicyT::template UniqueT<IdleQueue>; using IdleQueueVectorT = std::vector< IdleQueuePointerT, typename ResourcePolicyT::template AllocatorT<IdleQueuePointerT> >; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static bool constexpr c_bTrackContention_ = PolicyT::TrackContention() && !IsDispatch(DispatchFlags::Indirect, false), c_bIdleQueue_ = IsDispatch(DispatchFlags::IdleQueue, false), c_bLocality_ = IsDispatch(DispatchFlags::Locality, false), c_bTopology_ = IsDispatch(DispatchFlags::Topology, false); static size_t constexpr c_cbAlignment_ = ResourcePolicyT::InterferenceSize(), c_iInvalid_ = InvalidTopologyId(), c_nSparseDimension_ = ProcessingThread::c_nSparseDimension, //The lower count of busy processing threads required for task-stealing. c_nBusyStealingThreshold_ = 1, //Minimal processing thread queue depth required for task-stealing. c_nDepthStealingThreshold_ = 1, //Special flag used to mark pool's thread self-access when topology constraints applied. Highly unlikely that this //count of threads will ever be encountered. c_flgSelf_ = size_t(1) << (std::numeric_limits<size_t>::digits - 1), //Topological node mask, used to extract thread index. c_mskNode_ = ~c_flgSelf_; IdProvider::Id const c_idInstance_ alignas(c_cbAlignment_) = IdProvider::GenerateId(); size_t const c_nThreadsLimit_ = 0, c_nThreadGroup_ = 0, c_nIdleQueues_ = 0, c_mskLogThreads_ = 0, c_mskLogLogThreads_ = 0, c_mskDefaultTopology_ = 1; uint8_t const c_nGroupShift_ = 0, c_nLogThreads_ = 0, c_nLogLogThreads_ = 0; std::chrono::milliseconds const c_msIdleTimeout_ = std::chrono::milliseconds(0); IdleQueueVectorT const c_vIdleQueues_ alignas(c_cbAlignment_); math::FastMod const c_fmodThreads_ alignas(c_cbAlignment_); ProcessingThreadVectorT vThreads_ alignas(c_cbAlignment_); AtomicCounterT nSuspended_ alignas(c_cbAlignment_) = 0, nIdleDequeues_ alignas(c_cbAlignment_) = 0, nBusy_ alignas(c_cbAlignment_) = 0, nProcessed_ alignas(c_cbAlignment_) = 0, iStealingHint_ alignas(c_cbAlignment_) = 0; std::conditional_t<IsDispatch(DispatchFlags::Indirect, false), size_t, AtomicCounterT> iNextThread_ alignas(c_cbAlignment_) = 0; bool bCheckIdleThreads_ = false; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static size_t CalcThreadsLimitMask(size_t nLimit) noexcept { if constexpr(IsDispatch(DispatchFlags::Align2)) return CalcThreadsLimit(nLimit) - 1; else return std::has_single_bit(nLimit) ? nLimit - 1 : 0; } static uint8_t CalcLogThreads(size_t nLimit) noexcept { return std::max(static_cast<uint8_t>(std::countr_zero(std::bit_ceil(CalcThreadsLimit(nLimit)))), uint8_t(2)); } static uint8_t CalcLogLogThreads(size_t nLimit) noexcept { return std::max(static_cast<uint8_t>(std::countr_zero(std::bit_ceil(CalcLogThreads(nLimit)))), uint8_t(2)); } static size_t CalcGroupSize(size_t nGroup) noexcept { return std::bit_ceil(nGroup); } static size_t CalcThreadGroupSize(size_t nThreads) noexcept { size_t nGroup = PolicyT::ThreadGroupSize(nThreads); return CalcGroupSize(nGroup > 1 ? nGroup : nThreads); } static uint8_t CalcGroupShift(size_t nThreads) noexcept { size_t nGroup = CalcGroupSize(PolicyT::ThreadGroupSize(nThreads)); return nGroup > 1 ? static_cast<uint8_t>(std::countr_zero(std::bit_ceil(nGroup))) : std::numeric_limits<size_t>::digits - 1 ; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static ProcessingThreadPointerT MakeThread( Dispatcher* pDispatcher , size_t iThread , size_t mskTopology , SparseEdgesT&& aEdges #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin #endif ) { auto spThread = ResourcePolicyT::template AllocateShared<ProcessingThread>( pDispatcher //Topology node id is used with topology-constrained thread access, including task-stealing. , iThread //Topology constraints mask contains up to c_nLogLogThreads_ bits representing edges currently available for //inter-thread access (thread-to-thread task emission, task offloading, task stealing). , mskTopology //Sparse edges array is used in case of DispatchFlags::Sparse topology mode. It contains up to c_nLogLogThreads_ //indexes of neighbour threads allowed to access in context of current thread. , std::move(aEdges) #if FFPP_TRACK_ORIGIN , slOrigin #endif ); return spThread; } static ProcessingThreadPointerT MakeThread( Dispatcher* pDispatcher , size_t iThread #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin #endif ) { if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { return MakeThread( pDispatcher , iThread , pDispatcher->MakeTopologyMask() , pDispatcher->MakeSparseEdges(iThread) #if FFPP_TRACK_ORIGIN , slOrigin #endif ); } else { return MakeThread( pDispatcher , iThread , pDispatcher->MakeTopologyMask() , { } #if FFPP_TRACK_ORIGIN , slOrigin #endif ); } } static ProcessingThreadVectorT MakeThreadVector( Dispatcher* pDispatcher, size_t nThreads #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin #endif ) { ProcessingThreadVectorT vThreads; if constexpr(PolicyT::DispatchMethod() != DispatchFlags::Single) if(nThreads > 0) { vThreads.reserve(nThreads); if constexpr(IsDispatch(DispatchFlags::Static)) { std::generate_n( std::back_inserter(vThreads), nThreads, [ pDispatcher , nThreads , tMaskSizeBits = GetTopologyMaskParams(nThreads) , iThread = size_t(0) #if FFPP_TRACK_ORIGIN , slOrigin #endif ] () mutable { auto idNode = iThread++; if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { return MakeThread( pDispatcher , idNode , MakeTopologyMask(tMaskSizeBits) , MakeSparseEdges(tMaskSizeBits, nThreads, idNode) #if FFPP_TRACK_ORIGIN , slOrigin #endif ); } else { return MakeThread( pDispatcher , idNode , MakeTopologyMask(tMaskSizeBits) , { } #if FFPP_TRACK_ORIGIN , slOrigin #endif ); } } ); } else { vThreads.resize(nThreads); } } return vThreads; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// static IdleQueuePointerT MakeIdleQueue(size_t nCapacity) { return ResourcePolicyT::template AllocateUnique<IdleQueue>(nCapacity); } static std::pair<size_t, size_t> CalcIdleQueueSegments(size_t nThreads) { size_t nGroup = PolicyT::ThreadGroupSize(nThreads); nGroup = CalcGroupSize(nGroup > 1 ? nGroup : nThreads); return { nGroup, std::max(CalcGroupSize(nThreads), nGroup) / nGroup }; } static IdleQueueVectorT MakeIdleQueueVector(size_t nThreads) { IdleQueueVectorT vIdleQueues; if constexpr(c_bIdleQueue_) { auto const [nGroup, nSegments] = CalcIdleQueueSegments(nThreads); vIdleQueues.reserve(nSegments); for(size_t iGroup = 0; iGroup < nSegments; ++iGroup) { vIdleQueues.emplace_back(MakeIdleQueue(nGroup)); } } return vIdleQueues; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE size_t ToThreadIndex(size_t iAbsolute) const noexcept { if constexpr(IsDispatch(DispatchFlags::Align2)) return (iAbsolute & c_fmodThreads_.Mask()); else return c_fmodThreads_(iAbsolute); } FFPP_ATTR_INLINE size_t ToGroupIndex(size_t iThread) const noexcept { return iThread >> c_nGroupShift_; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE CounterT GetBusyCount() const noexcept { //The nBusy_ counter is expected to underflow. This is acceptable tradeoff because exact immediate value is not //required. In edge case when number of busy threads is near zero possible underflow may transiently influence //on dispatch and balancing algorithm's efficiency. CounterT nBusy = nBusy_.load(std::memory_order::relaxed); CounterT constexpr c_mskMSB = ~(std::numeric_limits<CounterT>::max() >> 1); return nBusy < c_mskMSB ? nBusy : 0; //expecting conditional move instruction here } FFPP_ATTR_INLINE bool IsUnderloaded() const noexcept { return GetBusyCount() < c_nThreadsLimit_; } FFPP_ATTR_INLINE IdleQueue* GetIdleQueue(size_t iThread) noexcept { return c_vIdleQueues_[ToGroupIndex(iThread)].get(); } FFPP_ATTR_INLINE void EnqueueIdleThread(size_t iThread) noexcept(IdleQueue::c_bNoexcept) { GetIdleQueue(iThread)->Enqueue(iThread); } template<bool t_bAscend> FFPP_ATTR_INLINE bool TraverseDequeueIdleThread(size_t iFrom, size_t& iThread) noexcept(IdleQueue::c_bNoexcept) { if constexpr(t_bAscend) { for(size_t iIQ = iFrom; iIQ < c_nIdleQueues_; ++iIQ) { if(c_vIdleQueues_[iIQ]->Dequeue(iThread)) return true; } } else { for(size_t iIQ = iFrom; iIQ-- > 0;) { if(c_vIdleQueues_[iIQ]->Dequeue(iThread)) return true; } } return false; } FFPP_ATTR_INLINE bool DequeueIdleThread(size_t& iThread) noexcept(IdleQueue::c_bNoexcept) { size_t constexpr c_nDirectionThreshold = size_t(1) << (std::numeric_limits<size_t>::digits - 1); if(c_nIdleQueues_ == 1) { return c_vIdleQueues_.front()->Dequeue(iThread); } else if(PolicyT::Random() > c_nDirectionThreshold) { return TraverseDequeueIdleThread<true>(0, iThread); } else { return TraverseDequeueIdleThread<false>(c_nIdleQueues_, iThread); } } FFPP_ATTR_INLINE bool DequeueIdleThreadRandom(size_t& iThread) noexcept(IdleQueue::c_bNoexcept) { size_t constexpr c_nDirectionThreshold = size_t(1) << (std::numeric_limits<size_t>::digits - 1); size_t const nRandom = PolicyT::Random(), iStart = math::MulHi(c_nIdleQueues_, nRandom); if(nRandom > c_nDirectionThreshold) { if(TraverseDequeueIdleThread<true>(iStart, iThread)) return true; if(TraverseDequeueIdleThread<false>(iStart, iThread)) return true; } else { if(TraverseDequeueIdleThread<false>(iStart, iThread)) return true; if(TraverseDequeueIdleThread<true>(iStart, iThread)) return true; } return false; } FFPP_ATTR_INLINE bool DequeueIdleThread(ProcessingThread* pptCurrent, size_t& iThread) noexcept(IdleQueue::c_bNoexcept) { if(pptCurrent == nullptr) return false; else return GetIdleQueue(pptCurrent->GetTopologyId())->Dequeue(iThread); } FFPP_ATTR_INLINE bool TryDequeueIdleThread(ProcessingThread* pptCurrent, size_t& iThread) noexcept(IdleQueue::c_bNoexcept) { if(DequeueIdleThread(pptCurrent, iThread)) return true; size_t constexpr c_nRandomThreshold = 5; if(c_nIdleQueues_ < c_nRandomThreshold) return DequeueIdleThread(iThread); else return DequeueIdleThreadRandom(iThread); } FFPP_ATTR_INLINE void ClearIdleQueue() noexcept(IdleQueue::c_bNoexcept) { for(size_t iIQ = 0; iIQ < c_nIdleQueues_; ++iIQ) { c_vIdleQueues_[iIQ]->Reset(); } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE static size_t ThreadWeight(ProcessingThread* const pThread) { size_t nWeight = 1; //Using existing idle threads is preferred over running new ones. if constexpr(IsDispatch(DispatchFlags::Static)) { //In Static mode threads always exist during pool's lifetime, there is no need to check pointer. size_t const nSize = pThread->Size(); nWeight = nSize ? nSize + 1 : 0; if constexpr(c_bTrackContention_) if(nWeight > 1) { //Current number of pending tasks weights more than contention level. nWeight += pThread->GetContentionLevel() >> 1; } } else if(pThread) { size_t const nSize = pThread->Size(); nWeight = nSize ? nSize + 1 : 0; //Use of existing idle threads is preferred over running new ones. if constexpr(c_bTrackContention_) if(nWeight > 1) { nWeight += pThread->GetContentionLevel() >> 1; } } return nWeight; } FFPP_ATTR_INLINE static size_t ThreadWeight(ProcessingThreadPointerT const& spThread) { return ThreadWeight(spThread.get()); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE void OnThreadEngage() noexcept { nBusy_.fetch_add(1, std::memory_order::relaxed); } template<bool t_bProcessed> FFPP_ATTR_INLINE void OnThreadDisengage(ProcessingThread* const pptDisengage) noexcept(!t_bProcessed) { size_t const nBusy = nBusy_.fetch_sub(1, std::memory_order::relaxed); if constexpr(t_bProcessed) { if constexpr(IsDispatch(DispatchFlags::Steal)) { bool bSteal = nBusy < c_nThreadsLimit_ && nBusy > c_nBusyStealingThreshold_; if(bSteal) bSteal = Steal(pptDisengage); if constexpr(IsDispatch(DispatchFlags::IdleQueue)) { if(!bSteal) EnqueueIdleThread(pptDisengage->GetTopologyId()); } } else { if constexpr(IsDispatch(DispatchFlags::IdleQueue)) { EnqueueIdleThread(pptDisengage->GetTopologyId()); } } } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE ProcessingThread* GetCurrentProcessingThread() noexcept { //Check for dispatcher id presence and compare it with current dispatcher instance id. This will filter threads of //another instance of the same thread pool specialization. In current point GetInstanceId() should always return //valid value. In case of any other outer thread l_tls_.idDispatcher will be set to invalid id (zero). ThreadLocalState const& tls = l_tls_; if(tls.idDispatcher != GetInstanceId()) return nullptr; //In this point current thread of execution definitely belongs to dispatcher of the current pool and should be //properly initialized. return tls.pptCurrent; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE size_t GetHypercubeNodeByMask(size_t iBase, size_t mskEdge) const noexcept { size_t iNode = iBase ^ mskEdge; if constexpr(IsDispatch(DispatchFlags::Align2)) return iNode; else return c_fmodThreads_(iNode); } FFPP_ATTR_INLINE size_t GetHypercubeNode(size_t iBase, size_t iEdge) const noexcept { return GetHypercubeNodeByMask(iBase, size_t(1) << iEdge); } FFPP_ATTR_INLINE size_t GetTopologyNode([[maybe_unused]] ProcessingThread* pptBase, [[maybe_unused]] size_t mskEdge) const { static_assert(c_bTopology_); if constexpr(IsDispatch(DispatchFlags::Hypercube)) { return GetHypercubeNodeByMask(pptBase->GetTopologyId(), mskEdge); } else if constexpr(IsDispatch(DispatchFlags::Sparse)) { return GetSparseNode(pptBase, mskEdge); } else static_assert(std::is_void_v<ProcessingThread>, "Invalid topology flags!"); return 0; } FFPP_ATTR_INLINE static std::tuple<size_t, size_t, size_t> GetTopologyMaskParams(size_t nLimit) noexcept { assert(nLimit != 0); size_t const nLogLimit = CalcLogThreads(nLimit), nLogLogLimit = CalcLogLogThreads(nLimit), mskLogLimit = (size_t(1) << nLogLimit) - 1, mskLogLogLimit = (size_t(1) << nLogLogLimit) - 1; if constexpr(IsDispatch(DispatchFlags::Sparse | DispatchFlags::Hypercube, true)) { return { mskLogLimit, nLogLimit, nLogLogLimit }; } else if constexpr(IsDispatch(DispatchFlags::Hypercube, true)) { return { mskLogLimit, nLogLimit, nLogLimit }; } else if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { return { mskLogLogLimit, nLogLogLimit, nLogLogLimit }; } else { return { mskLogLimit, nLogLimit, nLogLimit }; } } FFPP_ATTR_INLINE std::tuple<size_t, size_t, size_t> GetTopologyMaskParams() const noexcept { if constexpr(IsDispatch(DispatchFlags::Sparse | DispatchFlags::Hypercube, true)) { return { c_mskLogThreads_, c_nLogThreads_, c_nLogLogThreads_ }; } else if constexpr(IsDispatch(DispatchFlags::Hypercube, true)) { return { c_mskLogThreads_, c_nLogThreads_, c_nLogThreads_ }; } else if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { return { c_mskLogLogThreads_, c_nLogLogThreads_, c_nLogLogThreads_ }; } else { return { c_mskLogThreads_, c_nLogThreads_, c_nLogThreads_ }; } } template<bool t_bStrict = true> static size_t MakeTopologyMask(std::tuple<size_t, size_t, size_t> tMaskSizeBits) noexcept { auto const [mskSize, nSize, nBits] = tMaskSizeBits; if constexpr(IsDispatch(DispatchFlags::Sparse | DispatchFlags::Hypercube, true)) { if(nSize > 1) for(size_t nRandom = PolicyT::Random();;) { size_t mskTopology = nRandom & mskSize; if(mskTopology != 0) { if constexpr(t_bStrict) { if(size_t(std::popcount(mskTopology)) == nBits) { return mskTopology; } } else { if(size_t(std::popcount(mskTopology)) <= nBits) { return mskTopology; } } } if((nRandom >>= nSize) == 0) [[unlikely]] nRandom = PolicyT::Random(); } } return mskSize; } template<bool t_bStrict = false> FFPP_ATTR_INLINE size_t MakeTopologyMask() const noexcept { auto const [mskSize, nSize, nBits] = GetTopologyMaskParams(); assert(mskSize != 0); for(size_t nRandom = PolicyT::Random(); nRandom != 0; nRandom >>= nSize) { size_t const mskTopology = nRandom & mskSize; if(mskTopology != 0) { if constexpr(t_bStrict) { if(size_t(std::popcount(mskTopology)) == nBits) { return mskTopology; } } else { if(size_t(std::popcount(mskTopology)) <= nBits) { return mskTopology; } } } } return c_mskDefaultTopology_; } FFPP_ATTR_INLINE static SparseEdgesT MakeSparseEdges(std::tuple<size_t, size_t, size_t> tMaskSizeBits, size_t nLimit, size_t idBase) noexcept { SparseEdgesT aEdges { }; if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { auto const [mskSize, nSize, nEdges] = tMaskSizeBits; assert(nEdges < c_nSparseDimension_); for(size_t nRandom = PolicyT::Random(), nArbitrary = nRandom, iEdge = 0; iEdge < nEdges;) { if(size_t idNode = math::MulHi(nArbitrary, nLimit); idNode != idBase) { aEdges[iEdge++] = idNode; } nArbitrary = std::rotr(nArbitrary, int(nSize)); if(nArbitrary == nRandom) [[unlikely]] { nRandom = PolicyT::Random(); nArbitrary = nRandom; } } } return aEdges; } FFPP_ATTR_INLINE SparseEdgesT MakeSparseEdges(size_t idBase) const noexcept { auto const [mskSize, nSize, nEdges] = GetTopologyMaskParams(); assert(nEdges < c_nSparseDimension_); SparseEdgesT aEdges { }; for(size_t nRandom = PolicyT::Random(), nArbitrary = nRandom, iEdge = 0; iEdge < nEdges;) { if(size_t idNode = ToThreadIndex(nArbitrary); idNode != idBase) { aEdges[iEdge++] = idNode; } nArbitrary = std::rotr(nArbitrary, int(nSize)); if(nArbitrary == nRandom) [[unlikely]] { nRandom = PolicyT::Random(); nArbitrary = nRandom; } } return aEdges; } FFPP_ATTR_INLINE size_t ReshuffleEdge(size_t idBase, size_t idCurrent) const noexcept { auto const [mskSize, nSize, nEdges] = GetTopologyMaskParams(); for(size_t nRandom = PolicyT::Random(), nArbitrary = nRandom;;) { if(size_t idNode = ToThreadIndex(nArbitrary); idNode != idBase && idNode != idCurrent) { return idNode; } nArbitrary = std::rotr(nArbitrary, int(nSize)); if(nArbitrary == nRandom) [[unlikely]] { nRandom = PolicyT::Random(); nArbitrary = nRandom; } } return 0; } FFPP_ATTR_INLINE static size_t GetTopologyMask(ProcessingThread* pThread) noexcept { return pThread->GetTopologyMask(); } FFPP_ATTR_INLINE static size_t GetTopologyId(ProcessingThread* pThread) noexcept { return pThread->GetTopologyId(); } FFPP_ATTR_INLINE size_t GetSparseDimension() const noexcept { return c_nLogLogThreads_; } FFPP_ATTR_INLINE static SparseEdgesT const& GetSparseEdges(ProcessingThread* pThread) noexcept { return pThread->GetSparseEdges(); } FFPP_ATTR_INLINE static size_t GetSparseNode(ProcessingThread* pThread, size_t mskEdge) { return pThread->GetSparseNode(mskEdge); } FFPP_ATTR_INLINE void ReshuffleTopology(ProcessingThread* pThread) const noexcept { if constexpr(IsDispatch(DispatchFlags::Reshuffle)) { assert(pThread->IsProcessingThread()); if constexpr(IsDispatch(DispatchFlags::Sparse | DispatchFlags::Hypercube, true)) { pThread->UpdateTopologyMask(MakeTopologyMask()); } else if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { pThread->UpdateSparseEdges(MakeSparseEdges(pThread->GetTopologyId())); } } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE size_t SelectNodeRandom(size_t nRandom) const noexcept { return ToThreadIndex(nRandom); } FFPP_ATTR_INLINE size_t SelectNodeRandom(size_t nRandom, [[maybe_unused]] ProcessingThread* const pptCurrent) const noexcept { if constexpr(c_bTopology_) { //Topology constrained mode. if(pptCurrent != nullptr) [[likely]] { //Respect internal topology and current constraints. auto const [mskDimension, nDimension, nEdges] = GetTopologyMaskParams(); size_t //Neighbour threads and current thread as well. iEdge = math::MulHi(nRandom, nDimension + 1), mskEdge = size_t(1) << iEdge, mskTopology = GetTopologyMask(pptCurrent); //Neighbour node hit. if((mskTopology & mskEdge) != 0) return GetTopologyNode(pptCurrent, mskEdge); //Current node (self-hit) must be marked with c_flgSelf_. else return pptCurrent->GetTopologyId() | c_flgSelf_; } } //In case of outer-thread pool access select random thread index in the whole pool. return SelectNodeRandom(nRandom); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE bool TrySteal( ProcessingThread* pptThief, ProcessingThread* pptVictim, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { if(pptVictim && pptVictim != pptThief && pptVictim->Size() > c_nDepthStealingThreshold_) { idqTask = pptVictim->Steal(fnTask); if(idqTask != ProcessingThread::InvalidQueueId()) return true; pptThief->template OnTaskSteal<false>(); } return false; } FFPP_ATTR_INLINE void TryStealLinear( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { size_t const iStealingHint = iStealingHint_.fetch_add(1, std::memory_order::relaxed); for(size_t iEnd = iStealingHint + c_nThreadsLimit_, iThread = iStealingHint; iThread != iEnd; ++iThread) { auto const& spThread = vThreads_[ToThreadIndex(iThread)]; if(TrySteal(pptThief, spThread.get(), idqTask, fnTask)) { iStealingHint_.store(iThread + 1, std::memory_order::relaxed); return; } } } FFPP_ATTR_INLINE void TryStealRandom( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { static_assert(!c_bTopology_); auto const& spThread = vThreads_[SelectNodeRandom(PolicyT::Random())]; TrySteal(pptThief, spThread.get(), idqTask, fnTask); } FFPP_ATTR_INLINE void TryStealRandom2( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { static_assert(!c_bTopology_); for(size_t iProbe = 0, nRandom = PolicyT::Random(); iProbe < 2; ++iProbe) { auto const& spThread = vThreads_[SelectNodeRandom(nRandom)]; if(TrySteal(pptThief, spThread.get(), idqTask, fnTask)) return; nRandom = std::rotr(nRandom, std::numeric_limits<size_t>::digits / 2); } } FFPP_ATTR_INLINE void TryStealRandomL2( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { static_assert(!c_bTopology_); for(size_t iProbe = 0, nRandom = PolicyT::Random(), nArbitrary = nRandom; iProbe < c_nLogThreads_; ++iProbe) { auto const& spThread = vThreads_[SelectNodeRandom(nArbitrary)]; if(TrySteal(pptThief, spThread.get(), idqTask, fnTask)) return; nArbitrary = std::rotr(nArbitrary, int(c_nLogThreads_)); if(nArbitrary == nRandom) [[unlikely]] { nRandom = PolicyT::Random(); nArbitrary = nRandom; } } } FFPP_ATTR_INLINE void TryStealHypercube( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { static_assert(c_bTopology_); size_t const iThief = GetTopologyId(pptThief); for(uint8_t iEdge = 0; iEdge < c_nLogThreads_; ++iEdge) { auto const& spThread = vThreads_[GetHypercubeNode(iThief, iEdge)]; if(TrySteal(pptThief, spThread.get(), idqTask, fnTask)) return; } } FFPP_ATTR_INLINE bool TryStealSparseHypercube( size_t iThief, size_t mskConstraints, ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { static_assert(c_bTopology_); for(size_t mskCurrent = mskConstraints; mskCurrent != 0;) { size_t const mskEdge = mskCurrent & (~mskCurrent + 1); //size_t(1) << std::countr_zero(mskCurrent); auto const& spThread = vThreads_[GetHypercubeNodeByMask(iThief, mskEdge)]; if(TrySteal(pptThief, spThread.get(), idqTask, fnTask)) return true; mskCurrent &= ~mskEdge; } return false; } FFPP_ATTR_INLINE void TryStealSparseHypercube( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { size_t const iThief = GetTopologyId(pptThief), mskTopology = GetTopologyMask(pptThief); //First try to steal from up to c_nLogLogThreads_ neighbour threads according to current topology constraints mask. if(!TryStealSparseHypercube(iThief, mskTopology, pptThief, idqTask, fnTask)) [[unlikely]] { //If we got here constrained steal failed, so it is a good point to generate a new mask. ReshuffleTopology(pptThief); //Try to steal from previously unchecked neighbour threads. TryStealSparseHypercube(iThief, ~mskTopology & c_mskLogThreads_, pptThief, idqTask, fnTask); } } FFPP_ATTR_INLINE void TryStealSparse( ProcessingThread* pptThief, uint32_t& idqTask, typename ProcessingThread::FunctionalT& fnTask ) { static_assert(c_bTopology_); size_t const iThief = GetTopologyId(pptThief); auto const& aEdges = GetSparseEdges(pptThief); for(uint8_t iEdge = 0; iEdge < GetSparseDimension(); ++iEdge) { size_t const idEdge = aEdges[iEdge]; auto const& spThread = vThreads_[idEdge]; if(TrySteal(pptThief, spThread.get(), idqTask, fnTask)) return; if constexpr(IsDispatch(DispatchFlags::Reshuffle)) { //Edge failed, so it is reasonable to reshuffle it. pptThief->UpdateSparseEdge(iEdge, ReshuffleEdge(iThief, idEdge)); } } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FFPP_ATTR_INLINE size_t SelectThreadOptimal(ProcessingThread* const pptCurrent = nullptr) const { size_t iSelected = c_iInvalid_, nwMin = std::numeric_limits<size_t>::max(); if constexpr(c_bTopology_) { if(pptCurrent != nullptr) { //In case of inter-pool thread access loop only over current node's neighbour threads. auto const [mskDimension, nDimension, nEdges] = GetTopologyMaskParams(); size_t //Use current topology constraints mask or all neighbour nodes. mskCurrent = pptCurrent->GetTopologyMask(), //Start from current thread itself. iThread = pptCurrent->GetTopologyId(), nwThread = ThreadWeight(pptCurrent); do { if(nwThread == 0) { return iThread; } else if(nwThread < nwMin) { nwMin = nwThread; iSelected = iThread; } size_t const mskEdge = mskCurrent & (~mskCurrent + 1); //size_t(1) << std::countr_zero(mskCurrent); iThread = GetTopologyNode(pptCurrent, mskEdge); nwThread = ThreadWeight(vThreads_[iThread]); mskCurrent &= ~mskEdge; } while(mskCurrent != 0); constexpr bool c_bReshuffle = IsDispatch(DispatchFlags::Reshuffle); //&& !IsDispatch(DispatchFlags::Steal) if constexpr(c_bReshuffle) { if(iThread == pptCurrent->GetTopologyId() && nwMin > 1) { //In case of self-hit with at least one task already pending reshuffle topology. Each busy thread //tries to redirect future tasks to others in order to achieve even task distribution over the pool //and better overall parallelism. ReshuffleTopology(pptCurrent); } } } } //In case of no topology constraints or outer-thread pool access loop over all threads. if(!c_bTopology_ || iSelected == c_iInvalid_) { for(size_t iThread = 0; iThread < c_nThreadsLimit_; ++iThread) { size_t const nwThread = ThreadWeight(vThreads_[iThread]); if(nwThread == 0) { return iThread; } else if(nwThread < nwMin) { nwMin = nwThread; iSelected = iThread; } } } if(iSelected == c_iInvalid_) [[unlikely]] Except<>::Throw( "Invalid thread pool state @" #if FFPP_TRACK_ORIGIN , this->GetOrigin() #endif ); return iSelected; } FFPP_ATTR_INLINE size_t SelectThreadFill() noexcept { if constexpr(IsDispatch(DispatchFlags::Indirect)) return ToThreadIndex(iNextThread_++); else return ToThreadIndex(iNextThread_.fetch_add(1, std::memory_order::relaxed)); } FFPP_ATTR_INLINE size_t SelectThreadRandom(ProcessingThread* const pptCurrent = nullptr) const noexcept { return SelectNodeRandom(PolicyT::Random(), pptCurrent) & c_mskNode_; } FFPP_ATTR_INLINE size_t SelectThreadRandom2(ProcessingThread* const pptCurrent = nullptr) const noexcept { size_t constexpr nHalfBits = std::numeric_limits<size_t>::digits / 2; size_t const nRandom = PolicyT::Random(), nFlipped = std::rotr(nRandom, nHalfBits), iNode1 = c_bTopology_ ? SelectNodeRandom(nRandom, pptCurrent) : SelectNodeRandom(nRandom), iNode2 = c_bTopology_ ? SelectNodeRandom(nFlipped, pptCurrent) : SelectNodeRandom(nFlipped), iThread1 = iNode1 & c_mskNode_, iThread2 = iNode2 & c_mskNode_; auto const& spThread1 = vThreads_[iThread1]; auto const& spThread2 = vThreads_[iThread2]; if constexpr(!IsDispatch(DispatchFlags::Static)) { if(spThread1 == nullptr && spThread2 == nullptr) [[unlikely]] { return iThread1; } else if(spThread1 == nullptr) [[unlikely]] { if(spThread2->Size() == 0) return iThread2; return iThread1; } else if(spThread2 == nullptr) [[unlikely]] { if(spThread1->Size() == 0) return iThread1; return iThread2; } } auto const nSize1 = spThread1->Size(); if(nSize1 == 0) return iThread1; auto const nSize2 = spThread2->Size(); if(nSize2 == 0) return iThread2; if constexpr(c_bTopology_) { constexpr bool c_bReshuffle = IsDispatch(DispatchFlags::Reshuffle); //&& !IsDispatch(DispatchFlags::Steal) if(nSize1 < nSize2) { if constexpr(c_bReshuffle) { //In case of self-hit with pending tasks presence reshuffle topology. if((iNode1 & c_flgSelf_) != 0 && nSize1 > 1) ReshuffleTopology(spThread1.get()); } return iThread1; } else { if constexpr(c_bReshuffle) { if((iNode2 & c_flgSelf_) != 0 && nSize2 > 1) ReshuffleTopology(spThread2.get()); } return iThread2; } } else { return nSize1 < nSize2 ? iThread1 : iThread2; } } FFPP_ATTR_INLINE size_t GetRandomProbesL2([[maybe_unused]] ProcessingThread* const pptCurrent) const noexcept { if constexpr(c_bTopology_) if(pptCurrent != nullptr) { //We are in a context of a one of processing threads. if constexpr(IsDispatch(DispatchFlags::Sparse | DispatchFlags::Hypercube, true)) { //There are always at least (1 + c_nLogLogThreads_) nodes to select from (neighbors and self), //c_nLogLogThreads_ <- [2, 6]. return 2; } else if constexpr(IsDispatch(DispatchFlags::Hypercube, true)) { //Narrow probe count when inside unconstrained hypercube, use full dimension otherwise. return c_nLogLogThreads_; } else if constexpr(IsDispatch(DispatchFlags::Sparse, true)) { //There are always at least (1 + c_nSparseDimension_) nodes to select from (neighbors and self), //c_nSparseDimension_ <- [2, 6]. return 2; } } return c_nLogThreads_; } size_t SelectThreadRandomL2(ProcessingThread* const pptCurrent = nullptr) const { size_t const nProbes = GetRandomProbesL2(pptCurrent); size_t iSelected = c_iInvalid_, nwMin = std::numeric_limits<size_t>::max(); for(size_t nRandom = PolicyT::Random(), nArbitrary = nRandom, iProbe = 0; iProbe < nProbes; ++iProbe) { size_t const iNode = c_bTopology_ ? SelectNodeRandom(nArbitrary, pptCurrent) : SelectNodeRandom(nArbitrary), iThread = iNode & c_mskNode_, nWeight = ThreadWeight(vThreads_[iThread]); if(nWeight == 0) { //initialized and ready for processing idle thread return iThread; } else if(nWeight == 1) { //inactive (uninitialized) thread return c_bTopology_ ? (iNode & c_mskNode_) : iNode; } else if(nWeight < nwMin) { nwMin = nWeight; iSelected = iNode; } nArbitrary = std::rotr(nArbitrary, int(nProbes)); if(nArbitrary == nRandom) [[unlikely]] { nRandom = PolicyT::Random(); nArbitrary = nRandom; } } if(iSelected == c_iInvalid_) [[unlikely]] Except<>::Throw( "Invalid thread pool state @" #if FFPP_TRACK_ORIGIN , this->GetOrigin() #endif ); size_t const iThread = iSelected & c_mskNode_; constexpr bool c_bReshuffle = c_bTopology_ && IsDispatch(DispatchFlags::Reshuffle); //&& !IsDispatch(DispatchFlags::Steal) if constexpr(c_bReshuffle) { if((iSelected & c_flgSelf_) != 0 && nwMin > 1) { //In case of self-hit with at least one task already pending reshuffle topology. Each busy thread tries to //redirect future tasks to others in order to achieve even task distribution over the pool and better overall //parallelism. ReshuffleTopology(vThreads_[iThread].get()); } } return iThread; } size_t SelectThreadDispatch(size_t iHint = c_iInvalid_) { if(c_nThreadsLimit_ < 2) [[unlikely]] return 0; ProcessingThread* const pptCurrent = (c_bTopology_ || c_bLocality_) ? GetCurrentProcessingThread() : nullptr; if constexpr(c_bIdleQueue_) if(IsUnderloaded()) { //Check for busy threads counter first. size_t iThread = c_iInvalid_; if(TryDequeueIdleThread(pptCurrent, iThread) && iThread != c_iInvalid_) { return iThread; } } if constexpr(c_bLocality_) { //In case of internal (self) pool access, using actual processing thread. When accessing pool in context of //external thread using hint value which is the last processing thread index selected. This also helps in //dynamic thread pool mode when SelectThreadDispatch may be invoked in context of dispatcher thread when pool //is underloaded. if(pptCurrent != nullptr) return pptCurrent->GetTopologyId(); else if(iHint != c_iInvalid_) return iHint; } if constexpr(c_bTopology_) { if constexpr(IsDispatch(DispatchFlags::Optimal | DispatchFlags::RandomL2, true)) { //When topology constraints enabled it is reasonable to use randomized dispatch for external task emissions //and optimal dispatch for internal emissions and offloads (linear - O(log(c_nThreadsLimit_)) for hypercube, //O(log(log(c_nThreadsLimit_))) for sparse topologies). Note that this is optional - user must explicitly //specify (DispatchFlags::Optimal | DispatchFlags::RandomL2) dispatch method in combination with one of //topology modes (like DispatchFlags::Sparse). if(pptCurrent == nullptr) return SelectThreadRandomL2(); //outside of topological pool - external dispatch else return SelectThreadOptimal(pptCurrent); //inside topological pool - internal dispatch or offloading } else if constexpr(IsDispatch(DispatchFlags::Optimal | DispatchFlags::Random2, true)) { if(pptCurrent == nullptr) return SelectThreadRandom2(); else return SelectThreadOptimal(pptCurrent); } else if constexpr(IsDispatch(DispatchFlags::Optimal | DispatchFlags::Random, true)) { if(pptCurrent == nullptr) return SelectThreadRandom(); else return SelectThreadOptimal(pptCurrent); } } if constexpr(IsDispatch(DispatchFlags::Fill, true)) { return SelectThreadFill(); } else if constexpr(IsDispatch(DispatchFlags::Optimal, true)) { return SelectThreadOptimal(pptCurrent); } else if constexpr(IsDispatch(DispatchFlags::RandomL2, true)) { return SelectThreadRandomL2(pptCurrent); } else if constexpr(IsDispatch(DispatchFlags::Random2, true)) { return SelectThreadRandom2(pptCurrent); } else if constexpr(IsDispatch(DispatchFlags::Random, true)) { return SelectThreadRandom(pptCurrent); } else static_assert(std::is_void_v<ProcessingThread>, "Invalid task dispatch method!"); return c_iInvalid_; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// template<Concepts::Executable TExecutable> FFPP_ATTR_INLINE bool DispatchSingle( ResourcePolicyT::template SharedT<TExecutable> const& spExec, uint32_t nPriority, Flags flgExecutable ) { return this->Enqueue( [spExec] (auto* pSelf) { using TExecutablePointer = TExecutable*; bool constexpr c_bExceptionHandler = requires { PolicyT::OnProcessingException(TExecutablePointer { }, std::exception { }); }; if constexpr(c_bExceptionHandler) { FFPP_TRY { spExec->Process(); } FFPP_CATCH(std::exception, ex) { PolicyT::OnProcessingException(spExec.get(), ex); } } else { spExec->Process(); } pSelf->nProcessed_.fetch_add(1, std::memory_order::relaxed); }, nPriority, flgExecutable ); } template<Concepts::Executable TExecutable> FFPP_ATTR_HOT_PATH bool DispatchSingle(ResourcePolicyT::template SharedT<TExecutable> const& spExec) { if constexpr(IsScheduling<TExecutable>(SchedulingFlags::Deferred)) { return DispatchSingle(spExec, spExec->GetPriority(), spExec->GetInstanceFlags()); } else { //Thread bound execution serialization using nSubmits atomic counter. One and the same executable instance //spExec can be submitted for execution from multiple threads but only the first one (when nSubmits == 0) //will actually enqueue it to processing thread as much times as DispatchSingle will be called (simultaneously). AtomicCounterT& nSubmits = spExec->template GetSubmitsCounter<true>(); if(nSubmits.fetch_add(1, std::memory_order::relaxed) == 0) { Flags const flgExecutable = spExec->GetInstanceFlags(); uint32_t const nPriority = spExec->GetPriority(); UniqueGuard sgSubmits { [&nSubmits] () noexcept { nSubmits.store(0, std::memory_order::relaxed); } }; do { if(!DispatchSingle(spExec, nPriority, flgExecutable)) return false; } while(nSubmits.fetch_sub(1, std::memory_order::relaxed) > 1); sgSubmits.Abandon(); } return true; } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// template<Concepts::Executable TExecutable> FFPP_ATTR_INLINE bool DispatchDirect( ResourcePolicyT::template SharedT<TExecutable> const& spExec, uint32_t nPriority, Flags flgExecutable ) { if constexpr(IsScheduling<TExecutable>(SchedulingFlags::Concurrent)) { ProcessingThreadPointerT& spThread = vThreads_[SelectThreadDispatch()]; if(!spThread->Enqueue(spExec, nPriority, flgExecutable)) return false; } else { AtomicCounterT& nBinding = spExec->template GetBindingCounter<true>(); AtomicCounterT& idProcessor = spExec->template GetProcessorId<true>(); size_t iDispatch = idProcessor.load(std::memory_order::relaxed); UniqueGuard sgBinding { [&nBinding] () noexcept { nBinding.fetch_sub(1, std::memory_order::relaxed); } }; bool const bSelectThread = nBinding.fetch_add(1, std::memory_order::relaxed) == 0 && (!BindingEnabled<TExecutable>() || iDispatch == c_iInvalid_) ; if(bSelectThread) { iDispatch = SelectThreadDispatch(iDispatch); idProcessor.store(iDispatch, std::memory_order::relaxed); } ProcessingThreadPointerT& spThread = vThreads_[iDispatch]; if(!spThread->Enqueue(spExec, nPriority, flgExecutable)) return false; sgBinding.Abandon(); } return true; } template<Concepts::Executable TExecutable> FFPP_ATTR_HOT_PATH bool DispatchDirect(ResourcePolicyT::template SharedT<TExecutable> const& spExec) { if(this->IsComplete()) return false; if constexpr(IsScheduling<TExecutable>(SchedulingFlags::Deferred)) { return DispatchDirect(spExec, spExec->GetPriority(), spExec->GetInstanceFlags()); } else { Flags const flgExecutable = spExec->GetInstanceFlags(); uint32_t const nPriority = spExec->GetPriority(); //Thread bound execution serialization using nSubmits atomic counter. One and the same executable instance //spExec can be submitted for execution from multiple threads but only the first one (when nSubmits == 0) //will actually enqueue it to assigned pool's processing thread as much times as DispatchDirect will be called //(simultaneously). AtomicCounterT& nSubmits = spExec->template GetSubmitsCounter<true>(); if(nSubmits.fetch_add(1, std::memory_order::relaxed) != 0) return true; //Scope guards will act in case of exception or failure return. Otherwise they are manually released before //corresponding scope exit. UniqueGuard sgSubmits { [&nSubmits] () noexcept { nSubmits.store(0, std::memory_order::relaxed); } }; do { if(!DispatchDirect(spExec, nPriority, flgExecutable)) return false; } while(nSubmits.fetch_sub(1, std::memory_order::relaxed) > 1); sgSubmits.Abandon(); return true; } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// template<Concepts::Executable TExecutable> FFPP_ATTR_INLINE bool DispatchIndirect( ResourcePolicyT::template SharedT<TExecutable> const& spExec, uint32_t nPriority, Flags flgExecutable ) { return this->Enqueue([spExec, nPriority, flgExecutable] (auto* pThis) { if constexpr(IsScheduling<TExecutable>(SchedulingFlags::Concurrent)) { size_t const iDispatch = pThis->SelectThreadDispatch(); ProcessingThreadPointerT& spThread = pThis->vThreads_[iDispatch]; if constexpr(!IsDispatch(DispatchFlags::Static)) if(spThread == nullptr) { spThread = MakeThread( pThis , iDispatch #if FFPP_TRACK_ORIGIN , pThis->GetOrigin() #endif ); pThis->nSuspended_.fetch_sub(1, std::memory_order::relaxed); } if(!spThread->Enqueue(spExec, nPriority, flgExecutable)) { Except<>::Throw( "Failed to enqueue functional sequence to processing thread @" #if FFPP_TRACK_ORIGIN , spExec->GetOrigin() , pThis->GetOrigin() #endif ); } } else { AtomicCounterT& nBinding = spExec->template GetBindingCounter<true>(); UniqueGuard sgBinding { [&nBinding] () noexcept { nBinding.fetch_sub(1, std::memory_order::relaxed); } }; AtomicCounterT& idProcessor = spExec->template GetProcessorId<true>(); size_t iDispatch = idProcessor.load(std::memory_order::relaxed); bool const bSelectThread = nBinding.fetch_add(1, std::memory_order::relaxed) == 0 && (!BindingEnabled<TExecutable>() || iDispatch == c_iInvalid_) ; if(bSelectThread) { iDispatch = pThis->SelectThreadDispatch(iDispatch); idProcessor.store(iDispatch, std::memory_order::relaxed); } ProcessingThreadPointerT& spThread = pThis->vThreads_[iDispatch]; if constexpr(!IsDispatch(DispatchFlags::Static)) if(spThread == nullptr) { spThread = MakeThread( pThis , iDispatch #if FFPP_TRACK_ORIGIN , pThis->GetOrigin() #endif ); pThis->nSuspended_.fetch_sub(1, std::memory_order::relaxed); } if(!spThread->Enqueue(spExec, nPriority, flgExecutable)) { Except<>::Throw( "Failed to enqueue functional sequence to processing thread @" #if FFPP_TRACK_ORIGIN , spExec->GetOrigin() , pThis->GetOrigin() #endif ); } sgBinding.Abandon(); } }, nPriority, flgExecutable); } template<Concepts::Executable TExecutable> FFPP_ATTR_HOT_PATH bool DispatchIndirect(ResourcePolicyT::template SharedT<TExecutable> const& spExec) { if constexpr(IsScheduling<TExecutable>(SchedulingFlags::Deferred)) { return DispatchIndirect(spExec, spExec->GetPriority(), spExec->GetInstanceFlags()); } else { //Thread bound execution serialization using nSubmits atomic counter. One and the same executable instance //spExec can be submitted for execution from multiple threads but only the first one (when nSubmits == 0) //will actually enqueue it to dispatch thread as much times as DispatchIndirect will be called (simultaneously). AtomicCounterT& nSubmits = spExec->template GetSubmitsCounter<true>(); if(nSubmits.fetch_add(1, std::memory_order::relaxed) != 0) return true; UniqueGuard sgSubmits { [&nSubmits] () noexcept { nSubmits.store(0, std::memory_order::relaxed); } }; Flags const flgExecutable = spExec->GetInstanceFlags(); uint32_t const nPriority = spExec->GetPriority(); do { if(!DispatchIndirect(spExec, nPriority, flgExecutable)) return false; } while(nSubmits.fetch_sub(1, std::memory_order::relaxed) > 1); sgSubmits.Abandon(); return true; } } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool OnWait(Flags flgContext) noexcept { bool bWait = FunctionalQueueThreadT::OnWait(flgContext); if constexpr(!EqDispatch(DispatchFlags::Single)) { FFPP_TRY { if(!vThreads_.empty()) for(auto const& spThread : vThreads_) { if(spThread) { bWait = spThread->Wait(flgContext) && bWait; } } } FFPP_CATCH(std::exception, ex) { bWait = FunctionalQueueThreadT::OnWaitException(this, ex); } } return bWait; } void OnThreadTiming(auto const& tpTiming) { //never called when DispatchFlags::Static or DispatchFlags::Single specified //Check whether dispatcher instance construction completed. It is not impossible that OnThreadTiming handler could //be invoked before Dispatcher instance is actually ready. if(!this->IsThreadCommitted()) return; //Performing idle check as a series of per-group checks. This helps to interleave idle checks (and timed out //threads finalization) with other dispatcher tasks and keeps exclusive critical section duration under control. if(!bCheckIdleThreads_) { bCheckIdleThreads_ = this->Enqueue(MsgCheckIdleThreads { tpTiming, 0, std::min(c_nThreadsLimit_, c_nThreadGroup_) }); } } void On(MsgCheckIdleThreads msg) { //never called when DispatchFlags::Static or DispatchFlags::Single specified //Scope guard re-enables idle threads check when current series completed or in case of exception. UniqueGuard ugCheckIdleThreads { [this] () noexcept { bCheckIdleThreads_ = false; } }; ProcessingThreadVectorT vIdle; vIdle.reserve(msg.iStop - msg.iStart); //In direct dispatch mode (DispatchFlags::Indirect not specified) with dynamic thread pool maintenance must //be performed mutually exclusive with task submitting threads because idle threads will be stopped and destroyed. typename PolicyT::LockableT::UniqueLockT ul; if constexpr(!IsDispatch(DispatchFlags::Indirect)) ul = this->GetUniqueLock(); for(size_t iThread = msg.iStart; iThread < msg.iStop; ++iThread) { auto& spThread = vThreads_[iThread]; if(spThread && spThread->TestIdle(msg.tpTiming, c_msIdleTimeout_)) { vIdle.push_back(std::move(spThread)); } } if(!vIdle.empty()) { nSuspended_.fetch_add(vIdle.size(), std::memory_order::relaxed); if(ul.owns_lock()) ul.unlock(); for(auto const& spIdle : vIdle) spIdle->Finalize(Flags::Wait); } else if(ul.owns_lock()) { ul.unlock(); } //Performing idle check as a series of per-group checks. This helps to interleave idle checks (and timed out //threads finalization) with other dispatcher tasks and keeps exclusive critical section duration under control. if(msg.iStop < c_nThreadsLimit_) { bCheckIdleThreads_ = this->Enqueue(MsgCheckIdleThreads { msg.tpTiming, msg.iStop, std::min(c_nThreadsLimit_, msg.iStop + c_nThreadGroup_) }); //Idle threads check series is not completed yet. ugCheckIdleThreads.Abandon(); } else { //In dynamic thread pool stopped threads do not participate in dispatching from the idle-queue (though this is //technically acceptable in current pool's architecture). This behavior aligns with the pool's core principle //of maintaining only the necessary number of active threads. if constexpr(c_bIdleQueue_) if(nSuspended_.load(std::memory_order::relaxed) == c_nThreadsLimit_) { ClearIdleQueue(); } } } void On(PoolMetricsHandlerT& onPoolMetrics) { //Mutex not required here because only dispatcher thread can modify vThreads_ (in non-static modes). size_t nTasksPending = this->Size() - 1 , nThreadsActive = 1 , nContention = this->GetContentionLevel() , nOffloads = 0 , nProcessed = nProcessed_.exchange(0, std::memory_order::relaxed) , nIdleDequeues = 0 , nIdleFailures = 0 , nSuccessfulSteals = 0 , nFailedSteals = 0 , nReshuffles = 0 , nConnections = 0 ; for(auto& spThread : vThreads_) if(spThread) { nThreadsActive++; nTasksPending += spThread->Size(); nContention += spThread->GetContentionLevel(); nOffloads += spThread->Offloads(); nProcessed += spThread->Processed(); nSuccessfulSteals += spThread->template Steals<true>(); nFailedSteals += spThread->template Steals<false>(); nReshuffles += spThread->Reshuffles(); } if constexpr(c_bIdleQueue_) for(auto const& upIdleQueue : c_vIdleQueues_) { nIdleDequeues += upIdleQueue->Dequeues(); nIdleFailures += upIdleQueue->Failures(); } onPoolMetrics({ nThreadsActive , GetBusyCount() , this->Size() - 1 , nTasksPending , nContention , nProcessed , nIdleDequeues , nIdleFailures , nOffloads , nSuccessfulSteals , nFailedSteals , nReshuffles }); } };//Dispatcher ResourcePolicyT::template SharedT<Dispatcher> const c_sftDispatcher_; static ResourcePolicyT::template SharedT<Dispatcher> MakeDispatcher( size_t nThreadsLimit , std::chrono::seconds secIdleTimeout #if FFPP_TRACK_ORIGIN , std::source_location const& slOrigin #endif ) { using std::max; using std::chrono::milliseconds; using std::chrono::duration_cast; milliseconds constexpr c_msMinimalTimeout(500); return ResourcePolicyT::template AllocateShared<Dispatcher>( !!(DispatchFlags::Topology & PolicyT::DispatchMethod()) ? std::max(nThreadsLimit, size_t(2)) : nThreadsLimit , max(duration_cast<milliseconds>(secIdleTimeout) / 2, c_msMinimalTimeout) , max(duration_cast<milliseconds>(secIdleTimeout), c_msMinimalTimeout) #if FFPP_TRACK_ORIGIN , slOrigin #endif ); } };//FunctionalQueuePool }//FFPP #endif//FFPP_FUNCTIONAL_QUEUE_POOL_HPP