Cpp-Taskflow  2.2.0
executor.hpp
1 // 2019/08/03 - modified by Tsung-Wei Huang
2 // - made executor thread-safe
3 //
4 // 2019/07/26 - modified by Chun-Xun Lin
5 // - Combine explore_task & wait_for_task
6 // - Remove CAS operations
7 // - Update _num_thieves after pre_wait
8 // - TODO: Check will underutilization happen?
9 // - TODO: Find out uppper bound (does cycle exist?)
10 // - TODO: Does performance drop due to the busy looping after pre_wait?
11 //
12 // 2019/07/25 - modified by Tsung-Wei Huang & Chun-Xun Lin
13 // - fixed the potential underutilization
14 // - use CAS in both last thief & active worker to make the notification less aggressive
15 //
16 // 2019/06/18 - modified by Tsung-Wei Huang
17 // - fixed the cache to enable continuity
18 // - TODO: do we need a special optimization for 0 workers?
19 //
20 // 2019/06/11 - modified by Tsung-Wei Huang
21 // - fixed the bug in calling observer while the user
22 // may clear the data
23 // - added object pool for nodes
24 //
25 // 2019/05/17 - modified by Chun-Xun Lin
26 // - moved topology to taskflow
27 //
28 // 2019/05/14 - modified by Tsung-Wei Huang
29 // - isolated the executor from the taskflow
30 //
31 // 2019/04/09 - modified by Tsung-Wei Huang
32 // - removed silent_dispatch method
33 //
34 // 2019/03/12 - modified by Chun-Xun Lin
35 // - added taskflow
36 //
37 // 2019/02/11 - modified by Tsung-Wei Huang
38 // - refactored run_until
39 // - added allocator to topologies
40 // - changed to list for topologies
41 //
42 // 2019/02/10 - modified by Chun-Xun Lin
43 // - added run_n to execute taskflow
44 // - finished first peer-review with TW
45 //
46 // 2018/07 - 2019/02/09 - missing logs
47 //
48 // 2018/06/30 - created by Tsung-Wei Huang
49 // - added BasicTaskflow template
50 
51 // TODO items:
52 // 1. come up with a better way to remove the "joined" links
53 // during the execution of a static node (1st layer)
54 //
55 
56 #pragma once
57 
58 #include <iostream>
59 #include <vector>
60 #include <cstdlib>
61 #include <cstdio>
62 #include <random>
63 #include <atomic>
64 #include <memory>
65 #include <deque>
66 #include <optional>
67 #include <thread>
68 #include <algorithm>
69 #include <set>
70 #include <numeric>
71 #include <cassert>
72 
73 #include "spmc_queue.hpp"
74 #include "notifier.hpp"
75 #include "observer.hpp"
76 #include "taskflow.hpp"
77 
78 namespace tf {
79 
88 class Executor {
89 
90  struct Worker {
91  std::mt19937 rdgen { std::random_device{}() };
93  std::optional<Node*> cache;
94  };
95 
96  struct PerThread {
97  Executor* pool {nullptr};
98  int worker_id {-1};
99  };
100 
101  public:
102 
106  explicit Executor(unsigned n = std::thread::hardware_concurrency());
107 
111  ~Executor();
112 
120  std::future<void> run(Taskflow& taskflow);
121 
130  template<typename C>
131  std::future<void> run(Taskflow& taskflow, C&& callable);
132 
141  std::future<void> run_n(Taskflow& taskflow, size_t N);
142 
152  template<typename C>
153  std::future<void> run_n(Taskflow& taskflow, size_t N, C&& callable);
154 
164  template<typename P>
165  std::future<void> run_until(Taskflow& taskflow, P&& pred);
166 
177  template<typename P, typename C>
178  std::future<void> run_until(Taskflow& taskflow, P&& pred, C&& callable);
179 
183  void wait_for_all();
184 
190  size_t num_workers() const;
191 
205  template<typename Observer, typename... Args>
206  Observer* make_observer(Args&&... args);
207 
211  void remove_observer();
212 
213  private:
214 
215  std::condition_variable _topology_cv;
216  std::mutex _topology_mutex;
217  std::mutex _queue_mutex;
218 
219  unsigned _num_topologies {0};
220 
221  // scheduler field
222  std::vector<Worker> _workers;
224  std::vector<std::thread> _threads;
225 
227 
228  std::atomic<size_t> _num_actives {0};
229  std::atomic<size_t> _num_thieves {0};
230  std::atomic<bool> _done {0};
231 
232  Notifier _notifier;
233 
235 
236  unsigned _find_victim(unsigned);
237 
238  PerThread& _per_thread() const;
239 
240  bool _wait_for_task(unsigned, std::optional<Node*>&);
241 
242  void _spawn(unsigned);
243  void _exploit_task(unsigned, std::optional<Node*>&);
244  void _explore_task(unsigned, std::optional<Node*>&);
245  void _schedule(Node*, bool);
246  void _schedule(PassiveVector<Node*>&);
247  void _invoke(unsigned, Node*);
248  void _invoke_static_work(unsigned, Node*);
249  void _invoke_dynamic_work(unsigned, Node*, Subflow&);
250  void _init_module_node(Node*);
251  void _tear_down_topology(Topology*);
252  void _increment_topology();
253  void _decrement_topology();
254  void _decrement_topology_and_notify();
255 };
256 
257 // Constructor
258 inline Executor::Executor(unsigned N) :
259  _workers {N},
260  _waiters {N},
261  _notifier {_waiters} {
262  _spawn(N);
263 }
264 
265 // Destructor
267 
268  // wait for all topologies to complete
269  wait_for_all();
270 
271  // shut down the scheduler
272  _done = true;
273  _notifier.notify(true);
274 
275  for(auto& t : _threads){
276  t.join();
277  }
278 }
279 
280 // Function: num_workers
281 inline size_t Executor::num_workers() const {
282  return _workers.size();
283 }
284 
285 // Function: _per_thread
286 inline Executor::PerThread& Executor::_per_thread() const {
287  thread_local PerThread pt;
288  return pt;
289 }
290 
291 // Procedure: _spawn
292 inline void Executor::_spawn(unsigned N) {
293 
294  // Lock to synchronize all workers before creating _worker_maps
295  for(unsigned i=0; i<N; ++i) {
296  _threads.emplace_back([this, i] () -> void {
297 
298  PerThread& pt = _per_thread();
299  pt.pool = this;
300  pt.worker_id = i;
301 
302  std::optional<Node*> t;
303 
304  // must use 1 as condition instead of !done
305  while(1) {
306 
307  // execute the tasks.
308  _exploit_task(i, t);
309 
310  // wait for tasks
311  if(_wait_for_task(i, t) == false) {
312  break;
313  }
314  }
315 
316  });
317  }
318 }
319 
320 // Function: _find_victim
321 inline unsigned Executor::_find_victim(unsigned thief) {
322 
323  /*unsigned l = 0;
324  unsigned r = _workers.size() - 1;
325  unsigned vtm = std::uniform_int_distribution<unsigned>{l, r}(
326  _workers[thief].rdgen
327  );
328 
329  // try to look for a task from other workers
330  for(unsigned i=0; i<_workers.size(); ++i){
331 
332  if((thief == vtm && !_queue.empty()) ||
333  (thief != vtm && !_workers[vtm].queue.empty())) {
334  return vtm;
335  }
336 
337  if(++vtm; vtm == _workers.size()) {
338  vtm = 0;
339  }
340  } */
341 
342  // try to look for a task from other workers
343  for(unsigned vtm=0; vtm<_workers.size(); ++vtm){
344  if((thief == vtm && !_queue.empty()) ||
345  (thief != vtm && !_workers[vtm].queue.empty())) {
346  return vtm;
347  }
348  }
349 
350  return _workers.size();
351 }
352 
353 // Function: _explore_task
354 inline void Executor::_explore_task(unsigned thief, std::optional<Node*>& t) {
355 
356  //assert(_workers[thief].queue.empty());
357  assert(!t);
358 
359  const unsigned l = 0;
360  const unsigned r = _workers.size() - 1;
361 
362  const size_t F = (_workers.size() + 1) << 1;
363  const size_t Y = 100;
364 
365  size_t f = 0;
366  size_t y = 0;
367 
368  // explore
369  while(!_done) {
370 
371  unsigned vtm = std::uniform_int_distribution<unsigned>{l, r}(
372  _workers[thief].rdgen
373  );
374 
375  t = (vtm == thief) ? _queue.steal() : _workers[vtm].queue.steal();
376 
377  if(t) {
378  break;
379  }
380 
381  if(f++ > F) {
382  if(std::this_thread::yield(); y++ > Y) {
383  break;
384  }
385  }
386 
387  /*if(auto vtm = _find_victim(thief); vtm != _workers.size()) {
388  t = (vtm == thief) ? _queue.steal() : _workers[vtm].queue.steal();
389  // successful thief
390  if(t) {
391  break;
392  }
393  }
394  else {
395  if(f++ > F) {
396  if(std::this_thread::yield(); y++ > Y) {
397  break;
398  }
399  }
400  }*/
401  }
402 
403 }
404 
405 // Procedure: _exploit_task
406 inline void Executor::_exploit_task(unsigned i, std::optional<Node*>& t) {
407 
408  assert(!_workers[i].cache);
409 
410  if(t) {
411  auto& worker = _workers[i];
412  if(_num_actives.fetch_add(1) == 0 && _num_thieves == 0) {
413  _notifier.notify(false);
414  }
415  do {
416  _invoke(i, *t);
417 
418  if(worker.cache) {
419  t = *worker.cache;
420  worker.cache = std::nullopt;
421  }
422  else {
423  t = worker.queue.pop();
424  }
425 
426  } while(t);
427 
428  --_num_actives;
429  }
430 }
431 
432 // Function: _wait_for_task
433 inline bool Executor::_wait_for_task(unsigned me, std::optional<Node*>& t) {
434 
435  wait_for_task:
436 
437  assert(!t);
438 
439  ++_num_thieves;
440 
441  explore_task:
442 
443  if(_explore_task(me, t); t) {
444  if(auto N = _num_thieves.fetch_sub(1); N == 1) {
445  _notifier.notify(false);
446  }
447  return true;
448  }
449 
450  _notifier.prepare_wait(&_waiters[me]);
451 
452  //if(auto vtm = _find_victim(me); vtm != _workers.size()) {
453  if(!_queue.empty()) {
454 
455  _notifier.cancel_wait(&_waiters[me]);
456  //t = (vtm == me) ? _queue.steal() : _workers[vtm].queue.steal();
457 
458  if(t = _queue.steal(); t) {
459  if(auto N = _num_thieves.fetch_sub(1); N == 1) {
460  _notifier.notify(false);
461  }
462  return true;
463  }
464  else {
465  goto explore_task;
466  }
467  }
468 
469  if(_done) {
470  _notifier.cancel_wait(&_waiters[me]);
471  _notifier.notify(true);
472  --_num_thieves;
473  return false;
474  }
475 
476  if(_num_thieves.fetch_sub(1) == 1 && _num_actives) {
477  _notifier.cancel_wait(&_waiters[me]);
478  goto wait_for_task;
479  }
480 
481  // Now I really need to relinguish my self to others
482  _notifier.commit_wait(&_waiters[me]);
483 
484  return true;
485 }
486 
487 // Function: make_observer
488 template<typename Observer, typename... Args>
489 Observer* Executor::make_observer(Args&&... args) {
490  // use a local variable to mimic the constructor
491  auto tmp = std::make_unique<Observer>(std::forward<Args>(args)...);
492  tmp->set_up(_workers.size());
493  _observer = std::move(tmp);
494  return static_cast<Observer*>(_observer.get());
495 }
496 
497 // Procedure: remove_observer
499  _observer.reset();
500 }
501 
502 // Procedure: _schedule
503 // The main procedure to schedule a give task node.
504 // Each task node has two types of tasks - regular and subflow.
505 inline void Executor::_schedule(Node* node, bool bypass) {
506 
507  assert(_workers.size() != 0);
508 
509  // module node need another initialization
510  if(node->_module != nullptr && !node->_module->empty() && !node->is_spawned()) {
511  _init_module_node(node);
512  }
513 
514  // caller is a worker to this pool
515  if(auto& pt = _per_thread(); pt.pool == this) {
516  if(!bypass) {
517  _workers[pt.worker_id].queue.push(node);
518  }
519  else {
520  assert(!_workers[pt.worker_id].cache);
521  _workers[pt.worker_id].cache = node;
522  }
523  return;
524  }
525 
526  // other threads
527  {
528  std::scoped_lock lock(_queue_mutex);
529  _queue.push(node);
530  }
531 
532  _notifier.notify(false);
533 }
534 
535 // Procedure: _schedule
536 // The main procedure to schedule a set of task nodes.
537 // Each task node has two types of tasks - regular and subflow.
538 inline void Executor::_schedule(PassiveVector<Node*>& nodes) {
539 
540  assert(_workers.size() != 0);
541 
542  // We need to cacth the node count to avoid accessing the nodes
543  // vector while the parent topology is removed!
544  const auto num_nodes = nodes.size();
545 
546  if(num_nodes == 0) {
547  return;
548  }
549 
550  for(auto node : nodes) {
551  if(node->_module != nullptr && !node->_module->empty() && !node->is_spawned()) {
552  _init_module_node(node);
553  }
554  }
555 
556  // worker thread
557  if(auto& pt = _per_thread(); pt.pool == this) {
558  for(size_t i=0; i<num_nodes; ++i) {
559  _workers[pt.worker_id].queue.push(nodes[i]);
560  }
561  return;
562  }
563 
564  // other threads
565  {
566  std::scoped_lock lock(_queue_mutex);
567  for(size_t k=0; k<num_nodes; ++k) {
568  _queue.push(nodes[k]);
569  }
570  }
571 
572  if(num_nodes >= _workers.size()) {
573  _notifier.notify(true);
574  }
575  else {
576  for(size_t k=0; k<num_nodes; ++k) {
577  _notifier.notify(false);
578  }
579  }
580 }
581 
582 // Procedure: _init_module_node
583 inline void Executor::_init_module_node(Node* node) {
584 
585  node->_work = [node=node, this, tgt{PassiveVector<Node*>()}] () mutable {
586 
587  // second time to enter this context
588  if(node->is_spawned()) {
589  node->_dependents.resize(node->_dependents.size()-tgt.size());
590  for(auto& t: tgt) {
591  t->_successors.clear();
592  }
593  return ;
594  }
595 
596  // first time to enter this context
597  node->set_spawned();
598 
599  PassiveVector<Node*> src;
600 
601  for(auto& n: node->_module->_graph.nodes()) {
602  n->_topology = node->_topology;
603  if(n->num_dependents() == 0) {
604  src.push_back(n.get());
605  }
606  if(n->num_successors() == 0) {
607  n->precede(*node);
608  tgt.push_back(n.get());
609  }
610  }
611 
612  _schedule(src);
613  };
614 }
615 
616 // Procedure: _invoke
617 inline void Executor::_invoke(unsigned me, Node* node) {
618 
619  assert(_workers.size() != 0);
620 
621  // Here we need to fetch the num_successors first to avoid the invalid memory
622  // access caused by topology clear.
623  const auto num_successors = node->num_successors();
624 
625  // static task
626  // The default node work type. We only need to execute the callback if any.
627  if(auto index=node->_work.index(); index == 1) {
628  if(node->_module != nullptr) {
629  bool first_time = !node->is_spawned();
630  _invoke_static_work(me, node);
631  if(first_time) {
632  return ;
633  }
634  }
635  else {
636  _invoke_static_work(me, node);
637  }
638  }
639  // dynamic task
640  else if (index == 2){
641 
642  // Clear the subgraph before the task execution
643  if(!node->is_spawned()) {
644  if(node->_subgraph) {
645  node->_subgraph->clear();
646  }
647  else {
648  node->_subgraph.emplace();
649  }
650  }
651 
652  Subflow fb(*(node->_subgraph));
653 
654  _invoke_dynamic_work(me, node, fb);
655 
656  // Need to create a subflow if first time & subgraph is not empty
657  if(!node->is_spawned()) {
658  node->set_spawned();
659  if(!node->_subgraph->empty()) {
660  // For storing the source nodes
661  PassiveVector<Node*> src;
662  for(auto& n: node->_subgraph->nodes()) {
663  n->_topology = node->_topology;
664  n->set_subtask();
665  if(n->num_successors() == 0) {
666  if(fb.detached()) {
667  node->_topology->_num_sinks++;
668  }
669  else {
670  n->precede(*node);
671  }
672  }
673  if(n->num_dependents() == 0) {
674  src.push_back(n.get());
675  }
676  }
677 
678  _schedule(src);
679 
680  if(fb.joined()) {
681  return;
682  }
683  }
684  }
685  } // End of DynamicWork -----------------------------------------------------
686 
687  // Recover the runtime change due to dynamic tasking except the target & spawn tasks
688  // This must be done before scheduling the successors, otherwise this might cause
689  // race condition on the _dependents
690  //if(num_successors && !node->_subtask) {
691  if(!node->is_subtask()) {
692  // Only dynamic tasking needs to restore _dependents
693  // TODO:
694  if(node->_work.index() == 2 && !node->_subgraph->empty()) {
695  while(!node->_dependents.empty() && node->_dependents.back()->is_subtask()) {
696  node->_dependents.pop_back();
697  }
698  }
699  node->_num_dependents = static_cast<int>(node->_dependents.size());
700  node->unset_spawned();
701  }
702 
703  // At this point, the node storage might be destructed.
704  Node* cache {nullptr};
705 
706  for(size_t i=0; i<num_successors; ++i) {
707  if(--(node->_successors[i]->_num_dependents) == 0) {
708  if(cache) {
709  _schedule(cache, false);
710  }
711  cache = node->_successors[i];
712  }
713  }
714 
715  if(cache) {
716  _schedule(cache, true);
717  }
718 
719  // A node without any successor should check the termination of topology
720  if(num_successors == 0) {
721  if(--(node->_topology->_num_sinks) == 0) {
722  _tear_down_topology(node->_topology);
723  }
724  }
725 }
726 
727 // Procedure: _invoke_static_work
728 inline void Executor::_invoke_static_work(unsigned me, Node* node) {
729  if(_observer) {
730  _observer->on_entry(me, TaskView(node));
731  std::invoke(std::get<Node::StaticWork>(node->_work));
732  _observer->on_exit(me, TaskView(node));
733  }
734  else {
735  std::invoke(std::get<Node::StaticWork>(node->_work));
736  }
737 }
738 
739 // Procedure: _invoke_dynamic_work
740 inline void Executor::_invoke_dynamic_work(unsigned me, Node* node, Subflow& sf) {
741  if(_observer) {
742  _observer->on_entry(me, TaskView(node));
743  std::invoke(std::get<Node::DynamicWork>(node->_work), sf);
744  _observer->on_exit(me, TaskView(node));
745  }
746  else {
747  std::invoke(std::get<Node::DynamicWork>(node->_work), sf);
748  }
749 }
750 
751 // Function: run
753  return run_n(f, 1, [](){});
754 }
755 
756 // Function: run
757 template <typename C>
759  static_assert(std::is_invocable<C>::value);
760  return run_n(f, 1, std::forward<C>(c));
761 }
762 
763 // Function: run_n
764 inline std::future<void> Executor::run_n(Taskflow& f, size_t repeat) {
765  return run_n(f, repeat, [](){});
766 }
767 
768 // Function: run_n
769 template <typename C>
770 std::future<void> Executor::run_n(Taskflow& f, size_t repeat, C&& c) {
771  return run_until(f, [repeat]() mutable { return repeat-- == 0; }, std::forward<C>(c));
772 }
773 
774 // Function: run_until
775 template<typename P>
777  return run_until(f, std::forward<P>(pred), [](){});
778 }
779 
780 // Function: _tear_down_topology
781 inline void Executor::_tear_down_topology(Topology* tpg) {
782 
783  auto &f = tpg->_taskflow;
784 
785  //assert(&tpg == &(f._topologies.front()));
786 
787  // case 1: we still need to run the topology again
788  if(!std::invoke(tpg->_pred)) {
789  tpg->_recover_num_sinks();
790  _schedule(tpg->_sources);
791  }
792  // case 2: the final run of this topology
793  else {
794 
795  if(tpg->_call != nullptr) {
796  std::invoke(tpg->_call);
797  }
798 
799  f._mtx.lock();
800 
801  // If there is another run (interleave between lock)
802  if(f._topologies.size() > 1) {
803 
804  // Set the promise
805  tpg->_promise.set_value();
806  f._topologies.pop_front();
807  f._mtx.unlock();
808 
809  // decrement the topology but since this is not the last we don't notify
810  _decrement_topology();
811 
812  f._topologies.front()._bind(f._graph);
813  _schedule(f._topologies.front()._sources);
814  }
815  else {
816  assert(f._topologies.size() == 1);
817 
818  // Need to back up the promise first here becuz taskflow might be
819  // destroy before taskflow leaves
820  auto p {std::move(tpg->_promise)};
821 
822  f._topologies.pop_front();
823 
824  f._mtx.unlock();
825 
826  // We set the promise in the end in case taskflow leaves before taskflow
827  p.set_value();
828 
829  _decrement_topology_and_notify();
830  }
831  }
832 }
833 
834 // Function: run_until
835 template <typename P, typename C>
837 
838  // Predicate must return a boolean value
839  static_assert(std::is_invocable_v<C> && std::is_invocable_v<P>);
840 
841  _increment_topology();
842 
843  // Special case of predicate
844  if(f.empty() || std::invoke(pred)) {
845  std::promise<void> promise;
846  promise.set_value();
847  _decrement_topology_and_notify();
848  return promise.get_future();
849  }
850 
851  if(_workers.size() == 0) {
852  TF_THROW(Error::EXECUTOR, "no workers to execute the graph");
853  }
854 
858  //if(_workers.size() == 0) {
859  //
860  // Topology tpg(f, std::forward<P>(pred), std::forward<C>(c));
861 
862  // // Clear last execution data & Build precedence between nodes and target
863  // tpg._bind(f._graph);
864 
865  // std::stack<Node*> stack;
866 
867  // do {
868  // _schedule_unsync(tpg._sources, stack);
869  // while(!stack.empty()) {
870  // auto node = stack.top();
871  // stack.pop();
872  // _invoke_unsync(node, stack);
873  // }
874  // tpg._recover_num_sinks();
875  // } while(!std::invoke(tpg._pred));
876 
877  // if(tpg._call != nullptr) {
878  // std::invoke(tpg._call);
879  // }
880 
881  // tpg._promise.set_value();
882  //
883  // _decrement_topology_and_notify();
884  //
885  // return tpg._promise.get_future();
886  //}
887 
888  // Multi-threaded execution.
889  bool run_now {false};
890  Topology* tpg;
891  std::future<void> future;
892 
893  {
894  std::scoped_lock lock(f._mtx);
895 
896  // create a topology for this run
897  tpg = &(f._topologies.emplace_back(f, std::forward<P>(pred), std::forward<C>(c)));
898  future = tpg->_promise.get_future();
899 
900  if(f._topologies.size() == 1) {
901  run_now = true;
902  //tpg->_bind(f._graph);
903  //_schedule(tpg->_sources);
904  }
905  }
906 
907  // Notice here calling schedule may cause the topology to be removed sonner
908  // before the function leaves.
909  if(run_now) {
910  tpg->_bind(f._graph);
911  _schedule(tpg->_sources);
912  }
913 
914  return future;
915 }
916 
917 // Procedure: _increment_topology
918 inline void Executor::_increment_topology() {
919  std::scoped_lock lock(_topology_mutex);
920  ++_num_topologies;
921 }
922 
923 // Procedure: _decrement_topology_and_notify
924 inline void Executor::_decrement_topology_and_notify() {
925  std::scoped_lock lock(_topology_mutex);
926  if(--_num_topologies == 0) {
927  _topology_cv.notify_all();
928  }
929 }
930 
931 // Procedure: _decrement_topology
932 inline void Executor::_decrement_topology() {
933  std::scoped_lock lock(_topology_mutex);
934  --_num_topologies;
935 }
936 
937 // Procedure: wait_for_all
938 inline void Executor::wait_for_all() {
939  std::unique_lock lock(_topology_mutex);
940  _topology_cv.wait(lock, [&](){ return _num_topologies == 0; });
941 }
942 
943 } // end of namespace tf -----------------------------------------------------
944 
945 
std::future< void > run(Taskflow &taskflow)
runs the taskflow once
Definition: executor.hpp:752
void remove_observer()
removes the associated observer
Definition: executor.hpp:498
bool empty() const noexcept
queries if the queue is empty at the time of this call
Definition: spmc_queue.hpp:172
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:776
~Executor()
destructs the executor
Definition: executor.hpp:266
T yield(T... args)
void push(O &&item)
inserts an item to the queue
Definition: spmc_queue.hpp:189
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:869
Observer * make_observer(Args &&... args)
constructs an observer to inspect the activities of worker threads
Definition: executor.hpp:489
the class to create a task dependency graph
Definition: core/taskflow.hpp:15
A constant wrapper class to a task node, mainly used in the tf::ExecutorObserver interface.
Definition: task.hpp:370
bool empty() const
queries the emptiness of the taskflow
Definition: core/taskflow.hpp:122
bool joined() const
queries if the subflow will join its parent task
Definition: flow_builder.hpp:874
Lock-free unbounded single-producer multiple-consumer queue.
Definition: spmc_queue.hpp:29
The executor class to run a taskflow graph.
Definition: executor.hpp:88
size_t num_workers() const
queries the number of worker threads (can be zero)
Definition: executor.hpp:281
std::optional< T > steal()
steals an item from the queue
Definition: spmc_queue.hpp:239
Executor(unsigned n=std::thread::hardware_concurrency())
constructs the executor with N worker threads
Definition: executor.hpp:258
The building blocks of dynamic tasking.
Definition: flow_builder.hpp:817
std::future< void > run_n(Taskflow &taskflow, size_t N)
runs the taskflow for N times
Definition: executor.hpp:764
void wait_for_all()
wait for all pending graphs to complete
Definition: executor.hpp:938