19 #include "notifier.hpp" 20 #include "observer.hpp" 21 #include "taskflow.hpp" 39 std::optional<Node*> cache;
45 Worker* worker {
nullptr};
124 template<
typename P,
typename C>
152 template<
typename Observer,
typename... Args>
171 unsigned _num_topologies {0};
188 unsigned _find_victim(
unsigned);
190 PerThread& _per_thread()
const;
192 bool _wait_for_task(Worker&, std::optional<Node*>&);
194 void _spawn(
unsigned);
195 void _exploit_task(Worker&, std::optional<Node*>&);
196 void _explore_task(Worker&, std::optional<Node*>&);
197 void _schedule(Node*,
bool);
198 void _schedule(PassiveVector<Node*>&);
199 void _invoke(Worker&, Node*);
200 void _invoke_static_work(Worker&, Node*);
201 void _invoke_dynamic_work(Worker&, Node*,
Subflow&);
202 void _set_up_module_node(Node*);
203 void _set_up_topology(Topology*);
204 void _tear_down_topology(Topology**);
205 void _increment_topology();
206 void _decrement_topology();
207 void _decrement_topology_and_notify();
214 _notifier {_waiters} {
217 TF_THROW(Error::EXECUTOR,
"no workers to execute the graph");
231 _notifier.notify(
true);
233 for(
auto& t : _threads){
240 return _workers.size();
244 inline Executor::PerThread& Executor::_per_thread()
const {
245 thread_local PerThread pt;
251 if(
auto worker = _per_thread().worker; worker) {
260 inline void Executor::_spawn(
unsigned N) {
263 for(
unsigned i=0; i<N; ++i) {
267 _threads.emplace_back([
this] (Worker& w) ->
void {
269 PerThread& pt = _per_thread();
272 std::optional<Node*> t;
281 if(_wait_for_task(w, t) ==
false) {
286 }, std::ref(_workers[i]));
291 inline unsigned Executor::_find_victim(
unsigned thief) {
313 for(
unsigned vtm=0; vtm<_workers.size(); ++vtm){
314 if((thief == vtm && !_queue.
empty()) ||
315 (thief != vtm && !_workers[vtm].queue.empty())) {
320 return static_cast<unsigned>(_workers.size());
324 inline void Executor::_explore_task(Worker& thief, std::optional<Node*>& t) {
329 const unsigned l = 0;
330 const unsigned r =
static_cast<unsigned>(_workers.size()) - 1;
332 const size_t F = (_workers.size() + 1) << 1;
333 const size_t Y = 100;
343 t = (vtm == thief.id) ? _queue.
steal() : _workers[vtm].queue.steal();
374 inline void Executor::_exploit_task(Worker& worker, std::optional<Node*>& t) {
376 assert(!worker.cache);
379 if(_num_actives.fetch_add(1) == 0 && _num_thieves == 0) {
380 _notifier.notify(
false);
383 Topology *tpg = (*t)->_topology;
384 Node* prev_parent {(*t)->_parent};
385 worker.num_executed = 1;
408 worker.cache = std::nullopt;
411 t = worker.queue.pop();
414 if((*t)->_parent == prev_parent) {
415 worker.num_executed ++;
419 if(prev_parent ==
nullptr) {
421 (*t)->_topology->_join_counter.fetch_sub(worker.num_executed);
424 auto ret = prev_parent->_join_counter.fetch_sub(worker.num_executed);
425 if(ret == worker.num_executed) {
427 worker.queue.push(prev_parent);
430 worker.num_executed = 1;
431 prev_parent = (*t)->_parent;
436 if(prev_parent ==
nullptr) {
437 if(tpg->_join_counter.fetch_sub(worker.num_executed) == worker.num_executed) {
439 _tear_down_topology(&tpg);
441 t = worker.queue.pop();
443 worker.num_executed = 1;
449 if(prev_parent->_join_counter.fetch_sub(worker.num_executed) == worker.num_executed) {
451 worker.num_executed = 1;
452 prev_parent = prev_parent->_parent;
464 inline bool Executor::_wait_for_task(Worker& worker, std::optional<Node*>& t) {
474 if(_explore_task(worker, t); t) {
475 if(
auto N = _num_thieves.fetch_sub(1); N == 1) {
476 _notifier.notify(
false);
481 auto waiter = &_waiters[worker.id];
483 _notifier.prepare_wait(waiter);
486 if(!_queue.
empty()) {
488 _notifier.cancel_wait(waiter);
491 if(t = _queue.
steal(); t) {
492 if(
auto N = _num_thieves.fetch_sub(1); N == 1) {
493 _notifier.notify(
false);
503 _notifier.cancel_wait(waiter);
504 _notifier.notify(
true);
509 if(_num_thieves.fetch_sub(1) == 1 && _num_actives) {
510 _notifier.cancel_wait(waiter);
515 _notifier.commit_wait(waiter);
521 template<
typename Observer,
typename... Args>
524 auto tmp = std::make_unique<Observer>(std::forward<Args>(args)...);
525 tmp->set_up(_workers.size());
526 _observer = std::move(tmp);
527 return static_cast<Observer*
>(_observer.get());
538 inline void Executor::_schedule(Node* node,
bool bypass) {
543 if(node->_module && !node->_module->empty() && !node->_has_state(Node::SPAWNED)) {
544 _set_up_module_node(node);
548 if(
auto worker = _per_thread().worker; worker !=
nullptr) {
550 worker->queue.push(node);
553 assert(!worker->cache);
554 worker->cache = node;
561 std::scoped_lock lock(_queue_mutex);
565 _notifier.notify(
false);
571 inline void Executor::_schedule(PassiveVector<Node*>& nodes) {
577 const auto num_nodes = nodes.size();
583 for(
auto node : nodes) {
584 if(node->_module && !node->_module->empty() && !node->_has_state(Node::SPAWNED)) {
585 _set_up_module_node(node);
590 if(
auto worker = _per_thread().worker; worker !=
nullptr) {
591 for(
size_t i=0; i<num_nodes; ++i) {
592 worker->queue.push(nodes[i]);
599 std::scoped_lock lock(_queue_mutex);
600 for(
size_t k=0; k<num_nodes; ++k) {
601 _queue.
push(nodes[k]);
605 if(num_nodes >= _workers.size()) {
606 _notifier.notify(
true);
609 for(
size_t k=0; k<num_nodes; ++k) {
610 _notifier.notify(
false);
617 inline void Executor::_invoke(Worker& worker, Node* node) {
623 const auto num_successors = node->num_successors();
626 if(node->_work.index() == Node::CONDITION_WORK) {
628 if(node->_has_state(Node::BRANCH)) {
629 node->_join_counter = node->num_strong_dependents();
632 node->_join_counter = node->num_dependents();
635 if(
size_t id = std::get<Node::ConditionWork>(node->_work)();
id < num_successors) {
636 node->_successors[id]->_join_counter.store(0);
637 _schedule(node->_successors[
id],
true);
643 else if(
auto index=node->_work.index(); index == Node::STATIC_WORK) {
644 if(node->_module !=
nullptr) {
645 bool first_time = !node->_has_state(Node::SPAWNED);
646 _invoke_static_work(worker, node);
652 _invoke_static_work(worker, node);
656 else if (index == Node::DYNAMIC_WORK){
659 if(!node->_has_state(Node::SPAWNED)) {
660 if(node->_subgraph) {
661 node->_subgraph->clear();
664 node->_subgraph.emplace();
668 Subflow fb(*(node->_subgraph));
670 _invoke_dynamic_work(worker, node, fb);
673 if(!node->_has_state(Node::SPAWNED)) {
674 node->_set_state(Node::SPAWNED);
675 if(!node->_subgraph->empty()) {
677 PassiveVector<Node*> src;
679 for(
auto& n: node->_subgraph->nodes()) {
681 n->_topology = node->_topology;
682 n->_set_up_join_counter();
690 if(n->num_dependents() == 0) {
691 src.push_back(n.get());
695 const bool join = fb.
joined();
698 node->_topology->_join_counter.fetch_add(src.size());
702 node->_join_counter.fetch_add(src.size());
705 if(node->_parent ==
nullptr) {
706 node->_topology->_join_counter.fetch_add(1);
709 node->_parent->_join_counter.fetch_add(1);
726 if(node->_has_state(Node::BRANCH)) {
728 node->_join_counter = node->num_strong_dependents();
731 node->_join_counter = node->num_dependents();
734 node->_unset_state(Node::SPAWNED);
737 Node* cache {
nullptr};
738 size_t num_spawns {0};
740 auto& c = (node->_parent) ? node->_parent->_join_counter : node->_topology->_join_counter;
742 for(
size_t i=0; i<num_successors; ++i) {
743 if(--(node->_successors[i]->_join_counter) == 0) {
745 if(num_spawns == 0) {
746 c.fetch_add(num_successors);
749 _schedule(cache,
false);
751 cache = node->_successors[i];
756 worker.num_executed += (node->_successors.size() - num_spawns);
760 _schedule(cache,
true);
765 inline void Executor::_invoke_static_work(Worker& worker, Node* node) {
767 _observer->on_entry(worker.id,
TaskView(node));
768 std::invoke(std::get<Node::StaticWork>(node->_work));
769 _observer->on_exit(worker.id,
TaskView(node));
772 std::invoke(std::get<Node::StaticWork>(node->_work));
777 inline void Executor::_invoke_dynamic_work(Worker& worker, Node* node,
Subflow& sf) {
779 _observer->on_entry(worker.id,
TaskView(node));
780 std::invoke(std::get<Node::DynamicWork>(node->_work), sf);
781 _observer->on_exit(worker.id,
TaskView(node));
784 std::invoke(std::get<Node::DynamicWork>(node->_work), sf);
790 return run_n(f, 1, [](){});
794 template <
typename C>
796 static_assert(std::is_invocable<C>::value);
797 return run_n(f, 1, std::forward<C>(c));
802 return run_n(f, repeat, [](){});
806 template <
typename C>
808 return run_until(f, [repeat]()
mutable {
return repeat-- == 0; }, std::forward<C>(c));
814 return run_until(f, std::forward<P>(pred), [](){});
818 inline void Executor::_set_up_topology(Topology* tpg) {
820 tpg->_sources.clear();
823 for(
auto& node : tpg->_taskflow._graph.nodes()) {
825 node->_topology = tpg;
826 node->_clear_state();
828 if(node->num_dependents() == 0) {
829 tpg->_sources.push_back(node.get());
832 int join_counter = 0;
833 for(
auto p : node->_dependents) {
834 if(p->_work.index() == Node::CONDITION_WORK) {
835 node->_set_state(Node::BRANCH);
842 node->_join_counter.store(join_counter, std::memory_order_relaxed);
845 tpg->_join_counter.store(tpg->_sources.size(), std::memory_order_relaxed);
849 inline void Executor::_tear_down_topology(Topology** tpg) {
851 auto &f = (*tpg)->_taskflow;
856 if(!std::invoke((*tpg)->_pred)) {
859 assert((*tpg)->_join_counter == 0);
860 (*tpg)->_join_counter = (*tpg)->_sources.size();
862 _schedule((*tpg)->_sources);
867 if((*tpg)->_call !=
nullptr) {
868 std::invoke((*tpg)->_call);
874 if(f._topologies.size() > 1) {
876 assert((*tpg)->_join_counter == 0);
879 (*tpg)->_promise.set_value();
880 f._topologies.pop_front();
884 _decrement_topology();
886 *tpg = &(f._topologies.front());
888 _set_up_topology(*tpg);
889 _schedule((*tpg)->_sources);
901 assert(f._topologies.size() == 1);
905 auto p {std::move((*tpg)->_promise)};
907 f._topologies.pop_front();
914 _decrement_topology_and_notify();
923 template <
typename P,
typename C>
927 static_assert(std::is_invocable_v<C> && std::is_invocable_v<P>);
929 _increment_topology();
932 if(f.
empty() || std::invoke(pred)) {
935 _decrement_topology_and_notify();
936 return promise.get_future();
975 bool run_now {
false};
980 std::scoped_lock lock(f._mtx);
983 tpg = &(f._topologies.emplace_back(f, std::forward<P>(pred), std::forward<C>(c)));
984 future = tpg->_promise.get_future();
986 if(f._topologies.size() == 1) {
996 _set_up_topology(tpg);
997 _schedule(tpg->_sources);
1004 inline void Executor::_increment_topology() {
1005 std::scoped_lock<std::mutex> lock(_topology_mutex);
1010 inline void Executor::_decrement_topology_and_notify() {
1011 std::scoped_lock<std::mutex> lock(_topology_mutex);
1012 if(--_num_topologies == 0) {
1013 _topology_cv.notify_all();
1018 inline void Executor::_decrement_topology() {
1019 std::scoped_lock lock(_topology_mutex);
1026 _topology_cv.wait(lock, [&](){
return _num_topologies == 0; });
1031 inline void Executor::_set_up_module_node(Node* node) {
1033 node->_work = [node=node,
this] () {
1036 if(node->_has_state(Node::SPAWNED)) {
1041 node->_set_state(Node::SPAWNED);
1043 PassiveVector<Node*> src;
1045 for(
auto& n: node->_module->_graph.nodes()) {
1047 n->_topology = node->_topology;
1049 n->_set_up_join_counter();
1051 if(n->num_dependents() == 0) {
1052 src.push_back(n.get());
1056 node->_join_counter.fetch_add(src.size());
1061 if(node->_parent ==
nullptr) {
1062 node->_topology->_join_counter.fetch_add(1);
1065 node->_parent->_join_counter.fetch_add(1);
std::future< void > run(Taskflow &taskflow)
runs the taskflow once
Definition: executor.hpp:789
void remove_observer()
removes the associated observer
Definition: executor.hpp:531
bool empty() const noexcept
queries if the queue is empty at the time of this call
Definition: wsq.hpp:175
std::future< void > run_until(Taskflow &taskflow, P &&pred)
runs the taskflow multiple times until the predicate becomes true and then invokes a callback ...
Definition: executor.hpp:813
~Executor()
destructs the executor
Definition: executor.hpp:224
void push(O &&item)
inserts an item to the queue
Definition: wsq.hpp:192
Definition: taskflow.hpp:5
T hardware_concurrency(T... args)
bool detached() const
queries if the subflow will be detached from its parent task
Definition: flow_builder.hpp:346
Observer * make_observer(Args &&... args)
constructs an observer to inspect the activities of worker threads
Definition: executor.hpp:522
std::optional< unsigned > this_worker_id() const
queries the id of the caller thread in this executor
Definition: executor.hpp:250
the class to create a task dependency graph
Definition: core/taskflow.hpp:18
an immutable accessor class to a task node, mainly used in the tf::ExecutorObserver interface...
Definition: task.hpp:296
bool empty() const
queries the emptiness of the taskflow
Definition: core/taskflow.hpp:139
bool joined() const
queries if the subflow will join its parent task
Definition: flow_builder.hpp:351
Lock-free unbounded single-producer multiple-consumer queue.
Definition: wsq.hpp:27
The executor class to run a taskflow graph.
Definition: executor.hpp:33
size_t num_workers() const
queries the number of worker threads (can be zero)
Definition: executor.hpp:239
std::optional< T > steal()
steals an item from the queue
Definition: wsq.hpp:242
Executor(unsigned n=std::thread::hardware_concurrency())
constructs the executor with N worker threads
Definition: executor.hpp:211
The building blocks of dynamic tasking.
Definition: flow_builder.hpp:294
std::future< void > run_n(Taskflow &taskflow, size_t N)
runs the taskflow for N times
Definition: executor.hpp:801
void wait_for_all()
wait for all pending graphs to complete
Definition: executor.hpp:1024