// // MIT License // Copyright (c) 2020 Jonathan R. Madsen // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED // "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // // --------------------------------------------------------------- // Tasking class header file // // Class Description: // // This file creates the a class for handling a group of tasks that // can be independently joined // // --------------------------------------------------------------- // Author: Jonathan Madsen (Feb 13th 2018) // --------------------------------------------------------------- #pragma once #include "PTL/AutoLock.hh" #ifndef G4GMAKE #include "PTL/Config.hh" #endif #include "PTL/JoinFunction.hh" #include "PTL/ScopeDestructor.hh" #include "PTL/Task.hh" #include "PTL/ThreadData.hh" #include "PTL/ThreadPool.hh" #include "PTL/Types.hh" #include "PTL/VTask.hh" #include "PTL/VUserTaskQueue.hh" #include "PTL/detail/CxxBackports.hh" #include #include #include #include #include #include #include #include #include #include // IWYU pragma: keep #include #include #include #include #include #if defined(PTL_USE_TBB) # include // IWYU pragma: keep #endif namespace PTL { namespace internal { std::atomic_uintmax_t& task_group_counter(); ThreadPool* get_default_threadpool(); intmax_t get_task_depth(); } // namespace internal template class TaskGroup { public: //------------------------------------------------------------------------// template using container_type = std::vector; using tid_type = std::thread::id; using size_type = uintmax_t; using lock_t = Mutex; using atomic_int = std::atomic_intmax_t; using atomic_uint = std::atomic_uintmax_t; using condition_t = Condition; using ArgTp = decay_t; using result_type = Tp; using task_pointer = std::shared_ptr>; using task_list_t = container_type; using this_type = TaskGroup; using promise_type = std::promise; using future_type = std::future; using packaged_task_type = std::packaged_task; using future_list_t = container_type; using join_type = typename JoinFunction::Type; using iterator = typename future_list_t::iterator; using reverse_iterator = typename future_list_t::reverse_iterator; using const_iterator = typename future_list_t::const_iterator; using const_reverse_iterator = typename future_list_t::const_reverse_iterator; //------------------------------------------------------------------------// template using task_type = Task...>; //------------------------------------------------------------------------// public: // Constructor template TaskGroup(Func&& _join, ThreadPool* _tp = internal::get_default_threadpool()); template TaskGroup(ThreadPool* _tp = internal::get_default_threadpool(), enable_if_t::value, int> = 0); // Destructor ~TaskGroup(); // delete copy-construct TaskGroup(const this_type&) = delete; // define move-construct // NOLINTNEXTLINE(performance-noexcept-move-constructor) TaskGroup(this_type&& rhs) = default; // delete copy-assign TaskGroup& operator=(const this_type& rhs) = delete; // define move-assign // NOLINTNEXTLINE(performance-noexcept-move-constructor) TaskGroup& operator=(this_type&& rhs) = default; public: template std::shared_ptr operator+=(std::shared_ptr&& _task); // wait to finish void wait(); // increment (prefix) intmax_t operator++() { return ++(m_tot_task_count); } intmax_t operator++(int) { return (m_tot_task_count)++; } intmax_t operator--() { return --(m_tot_task_count); } intmax_t operator--(int) { return (m_tot_task_count)--; } // size intmax_t size() const { return m_tot_task_count.load(); } // get the locks/conditions lock_t& task_lock() { return m_task_lock; } condition_t& task_cond() { return m_task_cond; } // identifier uintmax_t id() const { return m_id; } // thread pool void set_pool(ThreadPool* tp) { m_pool = tp; } ThreadPool*& pool() { return m_pool; } ThreadPool* pool() const { return m_pool; } bool is_native_task_group() const { return (m_tbb_task_group) == nullptr; } bool is_main() const { return this_tid() == m_main_tid; } // check if any tasks are still pending intmax_t pending() { return m_tot_task_count.load(); } static void set_verbose(int level) { f_verbose = level; } ScopeDestructor get_scope_destructor(); void notify(); void notify_all(); void reserve(size_t _n) { m_task_list.reserve(_n); m_future_list.reserve(_n); } public: template std::shared_ptr> wrap(Func func, Args... args) { return operator+=(std::make_shared>( is_native_task_group(), m_depth, std::move(func), std::move(args)...)); } template enable_if_t::value, void> exec(Func func, Args... args); template enable_if_t::value, void> exec(Func func, Args... args); template void run(Func func, Args... args) { exec(std::move(func), std::move(args)...); } protected: template enable_if_t::value, void> local_exec(Func func, Args... args); template enable_if_t::value, void> local_exec(Func func, Args... args); // shorter typedefs using itr_t = iterator; using citr_t = const_iterator; using ritr_t = reverse_iterator; using critr_t = const_reverse_iterator; public: //------------------------------------------------------------------------// // Get tasks with non-void return types // future_list_t& get_tasks() { return m_future_list; } const future_list_t& get_tasks() const { return m_future_list; } //------------------------------------------------------------------------// // iterate over tasks with return type // itr_t begin() { return m_future_list.begin(); } itr_t end() { return m_future_list.end(); } citr_t begin() const { return m_future_list.begin(); } citr_t end() const { return m_future_list.end(); } citr_t cbegin() const { return m_future_list.begin(); } citr_t cend() const { return m_future_list.end(); } ritr_t rbegin() { return m_future_list.rbegin(); } ritr_t rend() { return m_future_list.rend(); } critr_t rbegin() const { return m_future_list.rbegin(); } critr_t rend() const { return m_future_list.rend(); } //------------------------------------------------------------------------// // wait to finish template ::value, int> = 0> inline Up join(Up accum = {}); //------------------------------------------------------------------------// // wait to finish template ::value && std::is_void::value, int> = 0> inline void join(); //------------------------------------------------------------------------// // wait to finish template ::value && !std::is_void::value, int> = 0> inline void join(); //------------------------------------------------------------------------// // clear the task result history void clear(); protected: //------------------------------------------------------------------------// // get the thread id static tid_type this_tid() { return std::this_thread::get_id(); } //------------------------------------------------------------------------// // get the task count atomic_int& task_count() { return m_tot_task_count; } const atomic_int& task_count() const { return m_tot_task_count; } protected: static int f_verbose; // Private variables uintmax_t m_id = internal::task_group_counter()++; intmax_t m_depth = internal::get_task_depth(); tid_type m_main_tid = std::this_thread::get_id(); atomic_int m_tot_task_count{ 0 }; lock_t m_task_lock = {}; condition_t m_task_cond = {}; join_type m_join{}; ThreadPool* m_pool = internal::get_default_threadpool(); tbb_task_group_t* m_tbb_task_group = nullptr; task_list_t m_task_list = {}; future_list_t m_future_list = {}; private: void internal_update(); }; } // namespace PTL namespace PTL { template template TaskGroup::TaskGroup(Func&& _join, ThreadPool* _tp) : m_join{ std::forward(_join) } , m_pool{ _tp } { internal_update(); } template template TaskGroup::TaskGroup(ThreadPool* _tp, enable_if_t::value, int>) : m_join{ []() {} } , m_pool{ _tp } { internal_update(); } // Destructor template TaskGroup::~TaskGroup() { { // task will decrement counter and then acquire the lock to notify // condition variable so acquiring lock here will prevent the // task group from being destroyed before this is completed AutoLock _lk{ m_task_lock, std::defer_lock }; if(!_lk.owns_lock()) _lk.lock(); } if(m_tbb_task_group) { auto* _arena = m_pool->get_task_arena(); _arena->execute([this]() { this->m_tbb_task_group->wait(); }); } delete m_tbb_task_group; this->clear(); } template template std::shared_ptr TaskGroup::operator+=(std::shared_ptr&& _task) { // thread-safe increment of tasks in task group operator++(); // copy the shared pointer to abstract instance m_task_list.push_back(_task); // return the derived instance return std::move(_task); } template void TaskGroup::wait() { auto _dtor = ScopeDestructor{ [&]() { if(m_tbb_task_group) { auto* _arena = m_pool->get_task_arena(); _arena->execute([this]() { this->m_tbb_task_group->wait(); }); } } }; ThreadData* data = ThreadData::GetInstance(); if(!data) return; // if no pool was initially present at creation if(!m_pool) { // check for master MT run-manager m_pool = internal::get_default_threadpool(); // if no thread pool created if(!m_pool) { if(f_verbose > 0) { fprintf(stderr, "%s @ %i :: Warning! nullptr to thread-pool (%p)\n", __FUNCTION__, __LINE__, static_cast(m_pool)); std::cerr << __FUNCTION__ << "@" << __LINE__ << " :: Warning! " << "nullptr to thread pool!" << std::endl; } return; } } ThreadPool* tpool = (m_pool) ? m_pool : data->thread_pool; VUserTaskQueue* taskq = (tpool) ? tpool->get_queue() : data->current_queue; bool _is_main = data->is_main; bool _within_task = data->within_task; auto is_active_state = [&]() { return (tpool->state()->load(std::memory_order_relaxed) != thread_pool::state::STOPPED); }; auto execute_this_threads_tasks = [&]() { if(!taskq) return; // only want to process if within a task if((!_is_main || tpool->size() < 2) && _within_task) { int bin = static_cast(taskq->GetThreadBin()); // const auto nitr = (tpool) ? tpool->size() : // Thread::hardware_concurrency(); while(this->pending() > 0) { if(!taskq->empty()) { auto _task = taskq->GetTask(bin); if(_task) (*_task)(); } } } }; // checks for validity if(!is_native_task_group()) { // for external threads if(!_is_main || tpool->size() < 2) return; } else if(f_verbose > 0) { if(!tpool || !taskq) { // something is wrong, didn't create thread-pool? fprintf(stderr, "%s @ %i :: Warning! nullptr to thread data (%p) or task-queue " "(%p)\n", __FUNCTION__, __LINE__, static_cast(tpool), static_cast(taskq)); } // return if thread pool isn't built else if(is_native_task_group() && !tpool->is_alive()) { fprintf(stderr, "%s @ %i :: Warning! thread-pool is not alive!\n", __FUNCTION__, __LINE__); } else if(!is_active_state()) { fprintf(stderr, "%s @ %i :: Warning! thread-pool is not active!\n", __FUNCTION__, __LINE__); } } intmax_t wake_size = 2; AutoLock _lock(m_task_lock, std::defer_lock); while(is_active_state()) { execute_this_threads_tasks(); // while loop protects against spurious wake-ups while(_is_main && pending() > 0 && is_active_state()) { // auto _wake = [&]() { return (wake_size > pending() || // !is_active_state()); // }; // lock before sleeping on condition if(!_lock.owns_lock()) _lock.lock(); // Wait until signaled that a task has been competed // Unlock mutex while wait, then lock it back when signaled // when true, this wakes the thread if(pending() >= wake_size) { m_task_cond.wait(_lock); } else { m_task_cond.wait_for(_lock, std::chrono::microseconds(100)); } // unlock if(_lock.owns_lock()) _lock.unlock(); } // if pending is not greater than zero, we are joined if(pending() <= 0) break; } if(_lock.owns_lock()) _lock.unlock(); intmax_t ntask = this->task_count().load(); if(ntask > 0) { std::stringstream ss; ss << "\nWarning! Join operation issue! " << ntask << " tasks still " << "are running!" << std::endl; std::cerr << ss.str(); this->wait(); } } template ScopeDestructor TaskGroup::get_scope_destructor() { auto& _counter = m_tot_task_count; auto& _task_cond = task_cond(); auto& _task_lock = task_lock(); return ScopeDestructor{ [&_task_cond, &_task_lock, &_counter]() { auto _count = --(_counter); if(_count < 1) { AutoLock _lk{ _task_lock }; _task_cond.notify_all(); } } }; } template void TaskGroup::notify() { AutoLock _lk{ m_task_lock }; m_task_cond.notify_one(); } template void TaskGroup::notify_all() { AutoLock _lk{ m_task_lock }; m_task_cond.notify_all(); } template template enable_if_t::value, void> TaskGroup::exec(Func func, Args... args) { if(MaxDepth > 0 && !m_tbb_task_group && ThreadData::GetInstance() && ThreadData::GetInstance()->task_depth > MaxDepth) { local_exec(std::move(func), std::move(args)...); } else { auto& _counter = m_tot_task_count; auto& _task_cond = task_cond(); auto& _task_lock = task_lock(); auto _task = wrap([&_task_cond, &_task_lock, &_counter, func, args...]() { auto* _tdata = ThreadData::GetInstance(); if(_tdata) ++(_tdata->task_depth); func(args...); auto _count = --(_counter); if(_tdata) --(_tdata->task_depth); if(_count < 1) { AutoLock _lk{ _task_lock }; _task_cond.notify_all(); } }); if(m_tbb_task_group) { auto* _arena = m_pool->get_task_arena(); auto* _tbb_task_group = m_tbb_task_group; auto* _ptask = _task.get(); _arena->execute([_tbb_task_group, _ptask]() { _tbb_task_group->run([_ptask]() { (*_ptask)(); }); }); } else { m_pool->add_task(std::move(_task)); } } } template template enable_if_t::value, void> TaskGroup::exec(Func func, Args... args) { if(MaxDepth > 0 && !m_tbb_task_group && ThreadData::GetInstance() && ThreadData::GetInstance()->task_depth > MaxDepth) { local_exec(std::move(func), std::move(args)...); } else { auto& _counter = m_tot_task_count; auto& _task_cond = task_cond(); auto& _task_lock = task_lock(); auto _task = wrap([&_task_cond, &_task_lock, &_counter, func, args...]() { auto* _tdata = ThreadData::GetInstance(); if(_tdata) ++(_tdata->task_depth); auto&& _ret = func(args...); auto _count = --(_counter); if(_tdata) --(_tdata->task_depth); if(_count < 1) { AutoLock _lk{ _task_lock }; _task_cond.notify_all(); } return std::forward(_ret); }); if(m_tbb_task_group) { auto* _arena = m_pool->get_task_arena(); auto* _tbb_task_group = m_tbb_task_group; auto* _ptask = _task.get(); _arena->execute([_tbb_task_group, _ptask]() { _tbb_task_group->run([_ptask]() { (*_ptask)(); }); }); } else { m_pool->add_task(std::move(_task)); } } } template template enable_if_t::value, void> TaskGroup::local_exec(Func func, Args... args) { auto* _tdata = ThreadData::GetInstance(); if(_tdata) ++(_tdata->task_depth); promise_type _p{}; m_future_list.emplace_back(_p.get_future()); func(args...); _p.set_value(); if(_tdata) --(_tdata->task_depth); } template template enable_if_t::value, void> TaskGroup::local_exec(Func func, Args... args) { auto* _tdata = ThreadData::GetInstance(); if(_tdata) ++(_tdata->task_depth); promise_type _p{}; m_future_list.emplace_back(_p.get_future()); _p.set_value(func(args...)); if(_tdata) --(_tdata->task_depth); } template template ::value, int>> inline Up TaskGroup::join(Up accum) { this->wait(); for(auto& itr : m_task_list) { using RetT = decay_tget())>; accum = std::move(m_join(std::ref(accum), std::forward(itr->get()))); } for(auto& itr : m_future_list) { using RetT = decay_t; accum = std::move(m_join(std::ref(accum), std::forward(itr.get()))); } this->clear(); return accum; } template template ::value && std::is_void::value, int>> inline void TaskGroup::join() { this->wait(); for(auto& itr : m_task_list) itr->get(); for(auto& itr : m_future_list) itr.get(); m_join(); this->clear(); } template template ::value && !std::is_void::value, int>> inline void TaskGroup::join() { this->wait(); for(auto& itr : m_task_list) { using RetT = decay_tget())>; m_join(std::forward(itr->get())); } for(auto& itr : m_future_list) { using RetT = decay_t; m_join(std::forward(itr.get())); } this->clear(); } template void TaskGroup::clear() { m_future_list.clear(); m_task_list.clear(); } template void TaskGroup::internal_update() { if(!m_pool) m_pool = internal::get_default_threadpool(); if(!m_pool) { std::stringstream ss{}; ss << "[TaskGroup]> " << __FUNCTION__ << "@" << __LINE__ << " :: nullptr to thread pool"; throw std::runtime_error(ss.str()); } if(m_pool->is_tbb_threadpool()) { m_tbb_task_group = new tbb_task_group_t{}; } } template int TaskGroup::f_verbose = 0; } // namespace PTL