Cpp-Taskflow  2.2.0
flow_builder.hpp
1 #pragma once
2 
3 #include "task.hpp"
4 
5 namespace tf {
6 
13 class FlowBuilder {
14 
15  public:
16 
22  FlowBuilder(Graph& graph);
23 
33  template <typename C>
34  Task emplace(C&& callable);
35 
45  template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>* = nullptr>
46  auto emplace(C&&... callables);
47 
51  template <typename C>
52  Task silent_emplace(C&& callable);
53 
57  template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>* = nullptr>
58  auto silent_emplace(C&&... callables);
59 
77  template <typename I, typename C>
78  std::pair<Task, Task> parallel_for(I beg, I end, C&& callable, size_t partitions = 0);
79 
97  template <typename I, typename C, std::enable_if_t<std::is_arithmetic_v<I>, void>* = nullptr >
98  std::pair<Task, Task> parallel_for(I beg, I end, I step, C&& callable, size_t partitions = 0);
99 
117  template <typename I, typename T, typename B>
118  std::pair<Task, Task> reduce(I beg, I end, T& result, B&& bop);
119 
135  template <typename I, typename T>
136  std::pair<Task, Task> reduce_min(I beg, I end, T& result);
137 
153  template <typename I, typename T>
154  std::pair<Task, Task> reduce_max(I beg, I end, T& result);
155 
177  template <typename I, typename T, typename B, typename U>
178  std::pair<Task, Task> transform_reduce(I beg, I end, T& result, B&& bop, U&& uop);
179 
204  template <typename I, typename T, typename B, typename P, typename U>
205  std::pair<Task, Task> transform_reduce(I beg, I end, T& result, B&& bop1, P&& bop2, U&& uop);
206 
212  Task placeholder();
213 
220  void precede(Task A, Task B);
221 
227  void linearize(std::vector<Task>& tasks);
228 
235 
242  void broadcast(Task A, std::vector<Task>& others);
243 
251 
258  void gather(std::vector<Task>& others, Task A);
259 
266  void gather(std::initializer_list<Task> others, Task A);
267 
268  private:
269 
270  Graph& _graph;
271 
272  template <typename L>
273  void _linearize(L&);
274 };
275 
276 // Constructor
277 inline FlowBuilder::FlowBuilder(Graph& graph) :
278  _graph {graph} {
279 }
280 
281 // Procedure: precede
282 inline void FlowBuilder::precede(Task from, Task to) {
283  from._node->precede(*(to._node));
284 }
285 
286 // Procedure: broadcast
288  from.precede(keys);
289 }
290 
291 // Procedure: broadcast
293  from.precede(keys);
294 }
295 
296 // Function: gather
298  to.gather(keys);
299 }
300 
301 // Function: gather
303  to.gather(keys);
304 }
305 
306 // Function: placeholder
308  auto& node = _graph.emplace_back();
309  return Task(node);
310 }
311 
312 // Function: parallel_for
313 template <typename I, typename C>
314 std::pair<Task, Task> FlowBuilder::parallel_for(I beg, I end, C&& c, size_t p){
315 
316  using category = typename std::iterator_traits<I>::iterator_category;
317 
318  auto S = placeholder();
319  auto T = placeholder();
320  auto D = std::distance(beg, end);
321 
322  // special case
323  if(D == 0) {
324  S.precede(T);
325  return std::make_pair(S, T);
326  }
327 
328  // default partition equals to the worker count
329  if(p == 0) {
330  p = std::max(unsigned{1}, std::thread::hardware_concurrency());
331  }
332 
333  size_t b = (D + p - 1) / p; // block size
334  size_t r = (D % p) ? D % p : p; // workers to take b
335  size_t w = 0; // worker id
336 
337  while(beg != end) {
338 
339  auto e = beg;
340  size_t g = (w++ >= r) ? b - 1 : b;
341 
342  // Case 1: random access iterator
343  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
344  size_t x = std::distance(beg, end);
345  std::advance(e, std::min(x, g));
346  }
347  // Case 2: non-random access iterator
348  else {
349  for(size_t i=0; i<g && e != end; ++e, ++i);
350  }
351 
352  // Create a task
353  auto task = emplace([beg, e, c] () mutable {
354  std::for_each(beg, e, c);
355  });
356  S.precede(task);
357  task.precede(T);
358 
359  // adjust the pointer
360  beg = e;
361  }
362 
363  return std::make_pair(S, T);
364 }
365 
366 /*// Function: parallel_for
367 template <typename I, typename C>
368 std::pair<Task, Task> FlowBuilder::parallel_for(I beg, I end, C&& c, size_t g) {
369 
370  using category = typename std::iterator_traits<I>::iterator_category;
371 
372  if(g == 0) {
373  auto d = std::distance(beg, end);
374  auto w = std::max(unsigned{1}, std::thread::hardware_concurrency());
375  g = (d + w - 1) / w;
376  }
377 
378  auto source = placeholder();
379  auto target = placeholder();
380 
381  while(beg != end) {
382 
383  auto e = beg;
384 
385  // Case 1: random access iterator
386  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
387  size_t r = std::distance(beg, end);
388  std::advance(e, std::min(r, g));
389  }
390  // Case 2: non-random access iterator
391  else {
392  for(size_t i=0; i<g && e != end; ++e, ++i);
393  }
394 
395  // Create a task
396  auto task = emplace([beg, e, c] () mutable {
397  std::for_each(beg, e, c);
398  });
399  source.precede(task);
400  task.precede(target);
401 
402  // adjust the pointer
403  beg = e;
404  }
405 
406  return std::make_pair(source, target);
407 }*/
408 
409 // Function: parallel_for
410 template <
411  typename I,
412  typename C,
413  std::enable_if_t<std::is_arithmetic_v<I>, void>*
414 >
415 std::pair<Task, Task> FlowBuilder::parallel_for(I beg, I end, I s, C&& c, size_t p) {
416 
417  using T = std::decay_t<I>;
418 
419  if((s == 0) || (beg < end && s <= 0) || (beg > end && s >=0) ) {
420  TF_THROW(Error::TASKFLOW,
421  "invalid range [", beg, ", ", end, ") with step size ", s
422  );
423  }
424 
425  // compute the distance
426  size_t D;
427 
428  if constexpr(std::is_integral_v<T>) {
429  if(beg <= end) {
430  D = (end - beg + s - 1) / s;
431  }
432  else {
433  D = (end - beg + s + 1) / s;
434  }
435  }
436  else if constexpr(std::is_floating_point_v<T>) {
437  D = static_cast<size_t>(std::ceil((end - beg) / s));
438  }
439  else {
440  static_assert(dependent_false_v<T>, "can't deduce distance");
441  }
442 
443  // source and target
444  auto source = placeholder();
445  auto target = placeholder();
446 
447  // special case
448  if(D == 0) {
449  source.precede(target);
450  return std::make_pair(source, target);
451  }
452 
453  // default partition equals to the worker count
454  if(p == 0) {
455  p = std::max(unsigned{1}, std::thread::hardware_concurrency());
456  }
457 
458  size_t b = (D + p - 1) / p; // block size
459  size_t r = (D % p) ? D % p : p; // workers to take b
460  size_t w = 0; // worker id
461 
462  // Integer indices
463  if constexpr(std::is_integral_v<T>) {
464  // positive case
465  if(beg < end) {
466  while(beg != end) {
467  auto g = (w++ >= r) ? b - 1 : b;
468  auto o = static_cast<T>(g) * s;
469  auto e = std::min(beg + o, end);
470  auto task = emplace([=] () mutable {
471  for(auto i=beg; i<e; i+=s) {
472  c(i);
473  }
474  });
475  source.precede(task);
476  task.precede(target);
477  beg = e;
478  }
479  }
480  // negative case
481  else if(beg > end) {
482  while(beg != end) {
483  auto g = (w++ >= r) ? b - 1 : b;
484  auto o = static_cast<T>(g) * s;
485  auto e = std::max(beg + o, end);
486  auto task = emplace([=] () mutable {
487  for(auto i=beg; i>e; i+=s) {
488  c(i);
489  }
490  });
491  source.precede(task);
492  task.precede(target);
493  beg = e;
494  }
495  }
496  }
497  // We enumerate the entire sequence to avoid floating error
498  else if constexpr(std::is_floating_point_v<T>) {
499  size_t N = 0;
500  size_t g = b;
501  auto B = beg;
502  for(auto i=beg; (beg<end ? i<end : i>end); i+=s, ++N) {
503  if(N == g) {
504  auto task = emplace([=] () mutable {
505  auto b = B;
506  for(size_t n=0; n<N; ++n) {
507  c(b);
508  b += s;
509  }
510  });
511  N = 0;
512  B = i;
513  source.precede(task);
514  task.precede(target);
515  if(++w >= r) {
516  g = b - 1;
517  }
518  }
519  }
520 
521  // the last pices
522  if(N != 0) {
523  auto task = emplace([=] () mutable {
524  auto b = B;
525  for(size_t n=0; n<N; ++n) {
526  c(b);
527  b += s;
528  }
529  });
530  source.precede(task);
531  task.precede(target);
532  }
533  }
534 
535  return std::make_pair(source, target);
536 }
537 
538 // Function: reduce_min
539 // Find the minimum element over a range of items.
540 template <typename I, typename T>
542  return reduce(beg, end, result, [] (const auto& l, const auto& r) {
543  return std::min(l, r);
544  });
545 }
546 
547 // Function: reduce_max
548 // Find the maximum element over a range of items.
549 template <typename I, typename T>
551  return reduce(beg, end, result, [] (const auto& l, const auto& r) {
552  return std::max(l, r);
553  });
554 }
555 
556 // Function: transform_reduce
557 template <typename I, typename T, typename B, typename U>
558 std::pair<Task, Task> FlowBuilder::transform_reduce(I beg, I end, T& result, B&& bop, U&& uop) {
559 
560  using category = typename std::iterator_traits<I>::iterator_category;
561 
562  // Even partition
563  size_t d = std::distance(beg, end);
564  size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
565  size_t g = std::max((d + w - 1) / w, size_t{2});
566 
567  auto source = placeholder();
568  auto target = placeholder();
569 
570  //std::vector<std::future<T>> futures;
571  auto g_results = std::make_unique<T[]>(w);
572  size_t id {0};
573 
574  while(beg != end) {
575 
576  auto e = beg;
577 
578  // Case 1: random access iterator
579  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
580  size_t r = std::distance(beg, end);
581  std::advance(e, std::min(r, g));
582  }
583  // Case 2: non-random access iterator
584  else {
585  for(size_t i=0; i<g && e != end; ++e, ++i);
586  }
587 
588  // Create a task
589  auto task = emplace([beg, e, bop, uop, res=&(g_results[id])] () mutable {
590  *res = uop(*beg);
591  for(++beg; beg != e; ++beg) {
592  *res = bop(std::move(*res), uop(*beg));
593  }
594  });
595 
596  source.precede(task);
597  task.precede(target);
598 
599  // adjust the pointer
600  beg = e;
601  id ++;
602  }
603 
604  // target synchronizer
605  target.work([&result, bop, res=MoC{std::move(g_results)}, w=id] () {
606  for(auto i=0u; i<w; i++) {
607  result = bop(std::move(result), res.object[i]);
608  }
609  });
610 
611  return std::make_pair(source, target);
612 }
613 
614 // Function: transform_reduce
615 template <typename I, typename T, typename B, typename P, typename U>
617  I beg, I end, T& result, B&& bop, P&& pop, U&& uop
618 ) {
619 
620  using category = typename std::iterator_traits<I>::iterator_category;
621 
622  // Even partition
623  size_t d = std::distance(beg, end);
624  size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
625  size_t g = std::max((d + w - 1) / w, size_t{2});
626 
627  auto source = placeholder();
628  auto target = placeholder();
629 
630  auto g_results = std::make_unique<T[]>(w);
631 
632  size_t id {0};
633  while(beg != end) {
634 
635  auto e = beg;
636 
637  // Case 1: random access iterator
638  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
639  size_t r = std::distance(beg, end);
640  std::advance(e, std::min(r, g));
641  }
642  // Case 2: non-random access iterator
643  else {
644  for(size_t i=0; i<g && e != end; ++e, ++i);
645  }
646 
647  // Create a task
648  auto task = emplace([beg, e, uop, pop, res= &g_results[id]] () mutable {
649  *res = uop(*beg);
650  for(++beg; beg != e; ++beg) {
651  *res = pop(std::move(*res), *beg);
652  }
653  });
654  //auto [task, future] = emplace([beg, e, uop, pop] () mutable {
655  // auto init = uop(*beg);
656  // for(++beg; beg != e; ++beg) {
657  // init = pop(std::move(init), *beg);
658  // }
659  // return init;
660  //});
661  source.precede(task);
662  task.precede(target);
663  //futures.push_back(std::move(future));
664 
665  // adjust the pointer
666  beg = e;
667  id ++;
668  }
669 
670  // target synchronizer
671  target.work([&result, bop, g_results=MoC{std::move(g_results)}, w=id] () {
672  for(auto i=0u; i<w; i++) {
673  result = bop(std::move(result), std::move(g_results.object[i]));
674  }
675  });
676  //target.work([&result, futures=MoC{std::move(futures)}, bop] () {
677  // for(auto& fu : futures.object) {
678  // result = bop(std::move(result), fu.get());
679  // }
680  //});
681 
682  return std::make_pair(source, target);
683 }
684 
686 //template <typename I>
687 //size_t FlowBuilder::_estimate_chunk_size(I beg, I end, I step) {
688 //
689 // using T = std::decay_t<I>;
690 //
691 // size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
692 // size_t N = 0;
693 //
694 // if constexpr(std::is_integral_v<T>) {
695 // if(beg <= end) {
696 // N = (end - beg + step - 1) / step;
697 // }
698 // else {
699 // N = (end - beg + step + 1) / step;
700 // }
701 // }
702 // else if constexpr(std::is_floating_point_v<T>) {
703 // N = static_cast<size_t>(std::ceil((end - beg) / step));
704 // }
705 // else {
706 // static_assert(dependent_false_v<T>, "can't deduce chunk size");
707 // }
708 //
709 // return (N + w - 1) / w;
710 //}
711 
712 
713 // Procedure: _linearize
714 template <typename L>
715 void FlowBuilder::_linearize(L& keys) {
716 
717  auto itr = keys.begin();
718  auto end = keys.end();
719 
720  if(itr == end) {
721  return;
722  }
723 
724  auto nxt = itr;
725 
726  for(++nxt; nxt != end; ++nxt, ++itr) {
727  itr->_node->precede(*(nxt->_node));
728  }
729 }
730 
731 // Procedure: linearize
733  _linearize(keys);
734 }
735 
736 // Procedure: linearize
738  _linearize(keys);
739 }
740 
741 // Proceduer: reduce
742 template <typename I, typename T, typename B>
743 std::pair<Task, Task> FlowBuilder::reduce(I beg, I end, T& result, B&& op) {
744 
745  using category = typename std::iterator_traits<I>::iterator_category;
746 
747  size_t d = std::distance(beg, end);
748  size_t w = std::max(unsigned{1}, std::thread::hardware_concurrency());
749  size_t g = std::max((d + w - 1) / w, size_t{2});
750 
751  auto source = placeholder();
752  auto target = placeholder();
753 
754  //T* g_results = static_cast<T*>(malloc(sizeof(T)*w));
755  auto g_results = std::make_unique<T[]>(w);
756  //std::vector<std::future<T>> futures;
757 
758  size_t id {0};
759  while(beg != end) {
760 
761  auto e = beg;
762 
763  // Case 1: random access iterator
764  if constexpr(std::is_same_v<category, std::random_access_iterator_tag>) {
765  size_t r = std::distance(beg, end);
766  std::advance(e, std::min(r, g));
767  }
768  // Case 2: non-random access iterator
769  else {
770  for(size_t i=0; i<g && e != end; ++e, ++i);
771  }
772 
773  // Create a task
774  //auto [task, future] = emplace([beg, e, op] () mutable {
775  auto task = emplace([beg, e, op, res = &g_results[id]] () mutable {
776  *res = *beg;
777  for(++beg; beg != e; ++beg) {
778  *res = op(std::move(*res), *beg);
779  }
780  //auto init = *beg;
781  //for(++beg; beg != e; ++beg) {
782  // init = op(std::move(init), *beg);
783  //}
784  //return init;
785  });
786  source.precede(task);
787  task.precede(target);
788  //futures.push_back(std::move(future));
789 
790  // adjust the pointer
791  beg = e;
792  id ++;
793  }
794 
795  // target synchronizer
796  //target.work([&result, futures=MoC{std::move(futures)}, op] () {
797  // for(auto& fu : futures.object) {
798  // result = op(std::move(result), fu.get());
799  // }
800  //});
801  target.work([g_results=MoC{std::move(g_results)}, &result, op, w=id] () {
802  for(auto i=0u; i<w; i++) {
803  result = op(std::move(result), g_results.object[i]);
804  }
805  });
806 
807  return std::make_pair(source, target);
808 }
809 
810 // ----------------------------------------------------------------------------
811 
817 class Subflow : public FlowBuilder {
818 
819  public:
820 
824  template <typename... Args>
825  Subflow(Args&&... args);
826 
830  void join();
831 
835  void detach();
836 
840  bool detached() const;
841 
845  bool joined() const;
846 
847  private:
848 
849  bool _detached {false};
850 };
851 
852 // Constructor
853 template <typename... Args>
854 Subflow::Subflow(Args&&... args) :
855  FlowBuilder {std::forward<Args>(args)...} {
856 }
857 
858 // Procedure: join
859 inline void Subflow::join() {
860  _detached = false;
861 }
862 
863 // Procedure: detach
864 inline void Subflow::detach() {
865  _detached = true;
866 }
867 
868 // Function: detached
869 inline bool Subflow::detached() const {
870  return _detached;
871 }
872 
873 // Function: joined
874 inline bool Subflow::joined() const {
875  return !_detached;
876 }
877 
878 // -----
879 
880 // Function: emplace
881 template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>*>
882 auto FlowBuilder::emplace(C&&... cs) {
883  return std::make_tuple(emplace(std::forward<C>(cs))...);
884 }
885 
886 // Function: emplace
887 template <typename C>
889  // dynamic tasking
890  if constexpr(std::is_invocable_v<C, Subflow&>) {
891  auto& n = _graph.emplace_back(
892  [c=std::forward<C>(c)] (Subflow& fb) mutable {
893  // first time execution
894  if(fb._graph.empty()) {
895  c(fb);
896  }
897  });
898  return Task(n);
899  }
900  // static tasking
901  else if constexpr(std::is_invocable_v<C>) {
902  auto& n = _graph.emplace_back(std::forward<C>(c));
903  return Task(n);
904  }
905  else {
906  static_assert(dependent_false_v<C>, "invalid task work type");
907  }
908 }
909 
910 // Function: silent_emplace
911 template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>*>
913  return std::make_tuple(emplace(std::forward<C>(cs))...);
914 }
915 
916 // Function: silent_emplace
917 template <typename C>
919  return emplace(std::forward<C>(c));
920 }
921 
922 // ---------- deprecated ----------
923 
925 //template <typename C>
926 //auto FlowBuilder::emplace(C&& c) {
927 // // subflow task
928 // if constexpr(std::is_invocable_v<C, Subflow&>) {
929 //
930 // using R = std::invoke_result_t<C, Subflow&>;
931 // std::promise<R> p;
932 // auto fu = p.get_future();
933 //
934 // if constexpr(std::is_same_v<void, R>) {
935 // auto& node = _graph.emplace_back([p=MoC(std::move(p)), c=std::forward<C>(c)]
936 // (Subflow& fb) mutable {
937 // if(fb._graph.empty()) {
938 // c(fb);
939 // // if subgraph is detached or empty after invoked
940 // if(fb.detached() || fb._graph.empty()) {
941 // p.get().set_value();
942 // }
943 // }
944 // else {
945 // p.get().set_value();
946 // }
947 // });
948 // return std::make_pair(Task(node), std::move(fu));
949 // }
950 // else {
951 // auto& node = _graph.emplace_back(
952 // [p=MoC(std::move(p)), c=std::forward<C>(c), r=std::optional<R>()]
953 // (Subflow& fb) mutable {
954 // if(fb._graph.empty()) {
955 // r.emplace(c(fb));
956 // if(fb.detached() || fb._graph.empty()) {
957 // p.get().set_value(std::move(*r));
958 // }
959 // }
960 // else {
961 // assert(r);
962 // p.get().set_value(std::move(*r));
963 // }
964 // });
965 // return std::make_pair(Task(node), std::move(fu));
966 // }
967 // }
968 // // regular task
969 // else if constexpr(std::is_invocable_v<C>) {
970 //
971 // using R = std::invoke_result_t<C>;
972 // std::promise<R> p;
973 // auto fu = p.get_future();
974 //
975 // if constexpr(std::is_same_v<void, R>) {
976 // auto& node = _graph.emplace_back(
977 // [p=MoC(std::move(p)), c=std::forward<C>(c)]() mutable {
978 // c();
979 // p.get().set_value();
980 // }
981 // );
982 // return std::make_pair(Task(node), std::move(fu));
983 // }
984 // else {
985 // auto& node = _graph.emplace_back(
986 // [p=MoC(std::move(p)), c=std::forward<C>(c)]() mutable {
987 // p.get().set_value(c());
988 // }
989 // );
990 // return std::make_pair(Task(node), std::move(fu));
991 // }
992 // }
993 // else {
994 // static_assert(dependent_false_v<C>, "invalid task work type");
995 // }
996 //}
997 
998 using SubflowBuilder = Subflow;
999 
1000 } // end of namespace tf. ---------------------------------------------------
void linearize(std::vector< Task > &tasks)
adds adjacent dependency links to a linear list of tasks
Definition: flow_builder.hpp:732
Task emplace(C &&callable)
creates a task from a given callable object
Definition: flow_builder.hpp:888
std::pair< Task, Task > parallel_for(I beg, I end, C &&callable, size_t partitions=0)
constructs a task dependency graph of range-based parallel_for
Definition: flow_builder.hpp:314
void broadcast(Task A, std::vector< Task > &others)
adds dependency links from one task A to many tasks
Definition: flow_builder.hpp:287
std::pair< Task, Task > transform_reduce(I beg, I end, T &result, B &&bop, U &&uop)
constructs a task dependency graph of parallel transformation and reduction
Definition: flow_builder.hpp:558
Task & gather(Ts &&... tasks)
adds precedence links from other tasks to this (same as succeed)
Definition: task.hpp:247
Definition: taskflow.hpp:5
T hardware_concurrency(T... args)
Task placeholder()
creates an empty task
Definition: flow_builder.hpp:307
Subflow(Args &&... args)
constructs a subflow builder object
Definition: flow_builder.hpp:854
void detach()
enables the subflow to detach from its parent task
Definition: flow_builder.hpp:864
bool detached() const
queries if the subflow will be detached from its parent task
Definition: flow_builder.hpp:869
std::pair< Task, Task > reduce_max(I beg, I end, T &result)
constructs a task dependency graph of parallel reduction through std::max
Definition: flow_builder.hpp:550
void precede(Task A, Task B)
adds a dependency link from task A to task B
Definition: flow_builder.hpp:282
void gather(std::vector< Task > &others, Task A)
adds dependency links from many tasks to one task A
Definition: flow_builder.hpp:297
FlowBuilder(Graph &graph)
construct a flow builder object
Definition: flow_builder.hpp:277
Building blocks of a task dependency graph.
Definition: flow_builder.hpp:13
bool joined() const
queries if the subflow will join its parent task
Definition: flow_builder.hpp:874
Handle to modify and access a task.
Definition: task.hpp:18
Task silent_emplace(C &&callable)
the same as tf::FlowBuilder::emplace (starting at 2.1.0)
Definition: flow_builder.hpp:918
Task & precede(Ts &&... tasks)
adds precedence links from this to other tasks
Definition: task.hpp:220
std::pair< Task, Task > reduce(I beg, I end, T &result, B &&bop)
construct a task dependency graph of parallel reduction
Definition: flow_builder.hpp:743
Task & work(C &&callable)
assigns a new callable object to the task
Definition: task.hpp:313
The building blocks of dynamic tasking.
Definition: flow_builder.hpp:817
void join()
enables the subflow to join its parent task
Definition: flow_builder.hpp:859
std::pair< Task, Task > reduce_min(I beg, I end, T &result)
constructs a task dependency graph of parallel reduction through std::min
Definition: flow_builder.hpp:541