]> https://gitweb.dealii.org/ - dealii.git/commitdiff
Import and require TaskFlow 3.10.
authorWolfgang Bangerth <bangerth@colostate.edu>
Tue, 14 Jan 2025 16:32:35 +0000 (09:32 -0700)
committerWolfgang Bangerth <bangerth@colostate.edu>
Tue, 14 Jan 2025 16:32:35 +0000 (09:32 -0700)
100 files changed:
bundled/setup_bundled.cmake
bundled/taskflow-3.10.0/LICENSE [new file with mode: 0644]
bundled/taskflow-3.10.0/README.md [moved from bundled/taskflow-3.7.0/README.md with 98% similarity]
bundled/taskflow-3.10.0/taskflow/algorithm/algorithm.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/algorithm/data_pipeline.hpp [moved from bundled/taskflow-3.7.0/taskflow/algorithm/data_pipeline.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/algorithm/find.hpp [moved from bundled/taskflow-3.7.0/taskflow/algorithm/find.hpp with 63% similarity]
bundled/taskflow-3.10.0/taskflow/algorithm/for_each.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/algorithm/module.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/algorithm/partitioner.hpp [moved from bundled/taskflow-3.7.0/taskflow/algorithm/partitioner.hpp with 95% similarity]
bundled/taskflow-3.10.0/taskflow/algorithm/pipeline.hpp [moved from bundled/taskflow-3.7.0/taskflow/algorithm/pipeline.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/algorithm/reduce.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/algorithm/scan.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/algorithm/sort.hpp [moved from bundled/taskflow-3.7.0/taskflow/algorithm/sort.hpp with 94% similarity]
bundled/taskflow-3.10.0/taskflow/algorithm/transform.hpp [moved from bundled/taskflow-3.7.0/taskflow/algorithm/transform.hpp with 63% similarity]
bundled/taskflow-3.10.0/taskflow/core/async.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/async.hpp with 66% similarity]
bundled/taskflow-3.10.0/taskflow/core/async_task.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/async_task.hpp with 85% similarity]
bundled/taskflow-3.10.0/taskflow/core/atomic_notifier.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/core/declarations.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/declarations.hpp with 98% similarity]
bundled/taskflow-3.10.0/taskflow/core/environment.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/environment.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/core/error.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/core/executor.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/executor.hpp with 71% similarity]
bundled/taskflow-3.10.0/taskflow/core/flow_builder.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/flow_builder.hpp with 84% similarity]
bundled/taskflow-3.10.0/taskflow/core/freelist.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/core/graph.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/core/nonblocking_notifier.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/notifier.hpp with 51% similarity]
bundled/taskflow-3.10.0/taskflow/core/observer.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/observer.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/core/runtime.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/core/semaphore.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/semaphore.hpp with 58% similarity]
bundled/taskflow-3.10.0/taskflow/core/task.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/task.hpp with 88% similarity]
bundled/taskflow-3.10.0/taskflow/core/taskflow.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/taskflow.hpp with 97% similarity]
bundled/taskflow-3.10.0/taskflow/core/topology.hpp [moved from bundled/taskflow-3.7.0/taskflow/core/topology.hpp with 83% similarity]
bundled/taskflow-3.10.0/taskflow/core/tsq.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/core/worker.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/find.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/find.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/for_each.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/for_each.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/matmul.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/matmul.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/merge.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/merge.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/reduce.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/reduce.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/scan.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/scan.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/sort.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/sort.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/transform.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/transform.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/algorithm/transpose.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/transpose.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_capturer.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_capturer.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_device.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_device.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_error.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_error.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_execution_policy.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_execution_policy.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_graph.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_graph.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_memory.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_memory.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_meta.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_meta.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_object.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_object.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_optimizer.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_optimizer.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_stream.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_stream.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cuda_task.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cuda_task.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/cuda/cudaflow.hpp [moved from bundled/taskflow-3.7.0/taskflow/cuda/cudaflow.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/connection.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/connection.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/dsl.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/dsl.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/meta_macro.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/meta_macro.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/task_analyzer.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/task_analyzer.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/task_dsl.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/task_dsl.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/task_trait.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/task_trait.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/tuple_utils.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/tuple_utils.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/dsl/type_list.hpp [moved from bundled/taskflow-3.7.0/taskflow/dsl/type_list.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/algorithm/reduce.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/algorithm/reduce.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/algorithm/sycl_for_each.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/algorithm/sycl_for_each.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/algorithm/sycl_transform.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/algorithm/sycl_transform.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/sycl_execution_policy.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/sycl_execution_policy.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/sycl_graph.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/sycl_graph.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/sycl_meta.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/sycl_meta.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/sycl_task.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/sycl_task.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/sycl/syclflow.hpp [moved from bundled/taskflow-3.7.0/taskflow/sycl/syclflow.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/taskflow.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/utility/iterator.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/utility/latch.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/utility/macros.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/macros.hpp with 52% similarity]
bundled/taskflow-3.10.0/taskflow/utility/math.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/utility/mpmc.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/utility/object_pool.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/object_pool.hpp with 98% similarity]
bundled/taskflow-3.10.0/taskflow/utility/os.hpp [new file with mode: 0644]
bundled/taskflow-3.10.0/taskflow/utility/serializer.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/serializer.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/utility/singleton.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/singleton.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/utility/small_vector.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/small_vector.hpp with 99% similarity]
bundled/taskflow-3.10.0/taskflow/utility/stream.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/stream.hpp with 100% similarity]
bundled/taskflow-3.10.0/taskflow/utility/traits.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/traits.hpp with 94% similarity]
bundled/taskflow-3.10.0/taskflow/utility/uuid.hpp [moved from bundled/taskflow-3.7.0/taskflow/utility/uuid.hpp with 100% similarity]
bundled/taskflow-3.7.0/LICENSE [deleted file]
bundled/taskflow-3.7.0/taskflow/algorithm/critical.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/algorithm/for_each.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/algorithm/launch.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/algorithm/reduce.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/algorithm/scan.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/core/error.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/core/executor-module-opt.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/core/graph.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/core/tsq.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/core/worker.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/taskflow.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/utility/iterator.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/utility/math.hpp [deleted file]
bundled/taskflow-3.7.0/taskflow/utility/os.hpp [deleted file]
cmake/configure/configure_10_taskflow.cmake

index 776cd76d148fad2fbad0a4fa872195dca7e6c357..c07ac543da8ba4419e8630c7b19e2c8b7faedbe2 100644 (file)
@@ -100,10 +100,10 @@ option(DEAL_II_FORCE_BUNDLED_TASKFLOW
   "Always use the bundled taskflow header library instead of an external one."
   OFF)
 
-set(TASKFLOW_FOLDER "${CMAKE_SOURCE_DIR}/bundled/taskflow-3.7.0")
+set(TASKFLOW_FOLDER "${CMAKE_SOURCE_DIR}/bundled/taskflow-3.10.0")
 
 macro(feature_taskflow_configure_bundled)
-  set(TASKFLOW_VERSION "3.7.0")
+  set(TASKFLOW_VERSION "3.10.0")
 
   list(APPEND DEAL_II_BUNDLED_INCLUDE_DIRS ${TASKFLOW_FOLDER})
 endmacro()
diff --git a/bundled/taskflow-3.10.0/LICENSE b/bundled/taskflow-3.10.0/LICENSE
new file mode 100644 (file)
index 0000000..132c3cc
--- /dev/null
@@ -0,0 +1,66 @@
+TASKFLOW MIT LICENSE 
+
+Copyright (c) 2018-2024 Dr. Tsung-Wei Huang
+
+The University of Wisconsin at Madison
+
+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/or sell
+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.
+
+---------------------------------
+The file cmake/CheckAtomic.cmake has the following license conditions.
+
+University of Illinois/NCSA Open Source License
+
+Copyright (c) 2003-2018 University of Illinois at Urbana-Champaign. All rights
+reserved.
+
+Developed by:
+
+LLVM Team
+
+University of Illinois at Urbana-Champaign
+
+http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+with the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimers.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimers in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the names of the LLVM Team, University of Illinois at
+  Urbana-Champaign, nor the names of its contributors may be used to endorse
+  or promote products derived from this Software without specific prior
+  written permission.
+
+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
+CONTRIBUTORS 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 WITH
+THE SOFTWARE.
similarity index 98%
rename from bundled/taskflow-3.7.0/README.md
rename to bundled/taskflow-3.10.0/README.md
index 5ea8d9ad2fea1fabb4662bb848a9dce1b5edc292..addfb3e923285000a05c1599e3d0dba2c5be80fa 100644 (file)
@@ -111,7 +111,7 @@ tell the compiler to include the [headers](./taskflow/).
 
 ```bash
 ~$ git clone https://github.com/taskflow/taskflow.git  # clone it only once
-~$ g++ -std=c++17 examples/simple.cpp -I. -O2 -pthread -o simple
+~$ g++ -std=c++20 examples/simple.cpp -I. -O2 -pthread -o simple
 ~$ ./simple
 TaskA
 TaskC 
@@ -381,13 +381,16 @@ To use Taskflow, you only need a compiler that supports C++17:
 + GNU C++ Compiler at least v8.4 with -std=c++17
 + Clang C++ Compiler at least v6.0 with -std=c++17
 + Microsoft Visual Studio at least v19.27 with /std:c++17
-+ AppleClang Xode Version at least v12.0 with -std=c++17
++ AppleClang Xcode Version at least v12.0 with -std=c++17
 + Nvidia CUDA Toolkit and Compiler (nvcc) at least v11.1 with -std=c++17
 + Intel C++ Compiler at least v19.0.1 with -std=c++17
 + Intel DPC++ Clang Compiler at least v13.0.0 with -std=c++17 and SYCL20
 
 Taskflow works on Linux, Windows, and Mac OS X.
 
+Although %Taskflow supports primarily C++17, you can enable C++20 compilation
+through `-std=c++20` to achieve better performance due to new C++20 features.
+
 # Learn More about Taskflow
 
 Visit our [project website][Project Website] and [documentation][documentation]
@@ -406,7 +409,7 @@ to learn more about Taskflow. To get involved:
 We are committed to support trustworthy developments for 
 both academic and industrial research projects in parallel 
 and heterogeneous computing. 
-If you are using Taskflow, please cite the following paper we publised at 2021 IEEE TPDS:
+If you are using Taskflow, please cite the following paper we published at 2021 IEEE TPDS:
 
 + Tsung-Wei Huang, Dian-Lun Lin, Chun-Xun Lin, and Yibo Lin, &quot;[Taskflow: A Lightweight Parallel and Heterogeneous Task Graph Computing System](https://tsung-wei-huang.github.io/papers/tpds21-taskflow.pdf),&quot; <i>IEEE Transactions on Parallel and Distributed Systems (TPDS)</i>, vol. 33, no. 6, pp. 1303-1320, June 2022
 
diff --git a/bundled/taskflow-3.10.0/taskflow/algorithm/algorithm.hpp b/bundled/taskflow-3.10.0/taskflow/algorithm/algorithm.hpp
new file mode 100644 (file)
index 0000000..63eb6a9
--- /dev/null
@@ -0,0 +1,14 @@
+#pragma once
+
+namespace tf {
+
+class Algorithm {
+
+  public:
+
+  template <typename T>
+  static auto make_module_task(T&&);
+
+};
+
+}  // end of namespace tf -----------------------------------------------------
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/algorithm/data_pipeline.hpp
rename to bundled/taskflow-3.10.0/taskflow/algorithm/data_pipeline.hpp
index 03935480b8df61531f5089864393bb9debcfe22f..3e0d203eae502737b67a8540fba7966f43a5c8d2 100644 (file)
@@ -189,7 +189,7 @@ using a module task in a taskflow.
 The only difference is that tf::DataPipeline provides a data abstraction
 for users to quickly express dataflow in a pipeline.
 The following example creates a data-parallel pipeline of three stages
-that generate dataflow from `void` to `int`, `std::string`, `float`, and `void`.
+that generate dataflow from `void` to `int`, `std::string`, and `void`.
 
 @code{.cpp}
 #include <taskflow/taskflow.hpp>
@@ -197,7 +197,7 @@ that generate dataflow from `void` to `int`, `std::string`, `float`, and `void`.
 
 int main() {
 
-  // data flow => void -> int -> std::string -> float -> void 
+  // data flow => void -> int -> std::string -> void
   tf::Taskflow taskflow("pipeline");
   tf::Executor executor;
 
similarity index 63%
rename from bundled/taskflow-3.7.0/taskflow/algorithm/find.hpp
rename to bundled/taskflow-3.10.0/taskflow/algorithm/find.hpp
index cb3d080c266b0def79f86c7ab2feaa9c80082a5b..60c79f8321ff899d9a01a0aef82f3e39747f1fe5 100644 (file)
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "launch.hpp"
+#include "../taskflow.hpp"
 
 namespace tf {
 
@@ -63,6 +63,8 @@ bool find_if_not_loop(
 template <typename B, typename E, typename T, typename UOP, typename P = DefaultPartitioner>
 auto make_find_if_task(B first, E last, T& result, UOP predicate, P part = P()) {
   
+  using namespace std::string_literals;
+  
   using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
   using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
 
@@ -77,60 +79,64 @@ auto make_find_if_task(B first, E last, T& result, UOP predicate, P part = P())
 
     // only myself - no need to spawn another graph
     if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        result = std::find_if(beg, end, predicate);
-      });
+      part([&](){ result = std::find_if(beg, end, predicate); })();
       return;
     }
+    
+    PreemptionGuard preemption_guard(rt);
 
+    // use no more workers than the iteration count
     if(N < W) {
       W = N;
     }
     
-    std::atomic<size_t> offset(N);
+    // we leverage smart pointer to let the last task update the result
+    std::shared_ptr<std::atomic<size_t>> offset(
+      new std::atomic<size_t>(N),
+      [=, &result](std::atomic<size_t>* p) {
+        result = std::next(beg, p->load(std::memory_order_relaxed));
+        delete p;
+      }
+    );
     
     // static partitioner
     if constexpr(part.type() == PartitionerType::STATIC) {
-
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-      
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-
-        launch_loop(W, w, rt, part,
-          [N, W, curr_b, chunk_size, beg, &predicate, &offset, &part] () mutable {
-            part.loop_until(N, W, curr_b, chunk_size,
-              [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
-                return detail::find_if_loop(
-                  offset, beg, prev_e, part_b, part_e, predicate
-                );
-              }
-            );
-          }
-        );
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop_until(N, W, curr_b, chunk_size,
+            [=, &offset, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+              return detail::find_if_loop(
+                *offset, beg, prev_e, part_b, part_e, predicate
+              );
+            }
+          );
+          // must release the ownership before async is destroyed
+          // as the node deletion comes after the join counter reaches zero
+          offset.reset();
+        });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
       }
-
-      rt.corun_all();
     }
     // dynamic partitioner
     else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, 
-        [N, W, beg, &predicate, &offset, &next, &part] () mutable {
-          part.loop_until(N, W, next, 
-            [&, prev_e=size_t{0}](size_t curr_b, size_t curr_e) mutable {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop_until(N, W, *next, 
+            [=, &offset, prev_e=size_t{0}](size_t curr_b, size_t curr_e) mutable {
               return detail::find_if_loop(
-                offset, beg, prev_e, curr_b, curr_e, predicate
+                *offset, beg, prev_e, curr_b, curr_e, predicate
               );
             }
           );
-        }
-      );
+          // must release the ownership before async is destroyed
+          // as the node deletion comes after the join counter reaches zero
+          offset.reset();
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
     }
-
-    // update the result iterator by the offset
-    result = std::next(beg, offset.load(std::memory_order_relaxed));
   };
 }
 
@@ -138,6 +144,8 @@ auto make_find_if_task(B first, E last, T& result, UOP predicate, P part = P())
 template <typename B, typename E, typename T, typename UOP, typename P = DefaultPartitioner>
 auto make_find_if_not_task(B first, E last, T& result, UOP predicate, P part = P()) {
   
+  using namespace std::string_literals;
+  
   using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
   using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
 
@@ -152,66 +160,71 @@ auto make_find_if_not_task(B first, E last, T& result, UOP predicate, P part = P
 
     // only myself - no need to spawn another graph
     if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        result = std::find_if_not(beg, end, predicate);
-      });
+      part([&](){ result = std::find_if_not(beg, end, predicate); })();
       return;
     }
 
+    PreemptionGuard preemption_guard(rt);
+
     if(N < W) {
       W = N;
     }
     
-    std::atomic<size_t> offset(N);
-    
+    // we leverage smart pointer to let the last task update the result
+    std::shared_ptr<std::atomic<size_t>> offset(
+      new std::atomic<size_t>(N),
+      [=, &result](std::atomic<size_t>* p) {
+        result = std::next(beg, p->load(std::memory_order_relaxed));
+        delete p;
+      }
+    );
+
     // static partitioner
     if constexpr(part.type() == PartitionerType::STATIC) {
-
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-      
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-
-        launch_loop(W, w, rt, part,
-          [N, W, curr_b, chunk_size, beg, &predicate, &offset, &part] () mutable {
-            part.loop_until(N, W, curr_b, chunk_size,
-              [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
-                return detail::find_if_not_loop(
-                  offset, beg, prev_e, part_b, part_e, predicate
-                );
-              }
-            );
-          }
-        );
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop_until(N, W, curr_b, chunk_size,
+            [=, &offset, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+              return detail::find_if_not_loop(
+                *offset, beg, prev_e, part_b, part_e, predicate
+              );
+            }
+          );
+          // must release the ownership before async is destroyed
+          // as the node deletion comes after the join counter reaches zero
+          offset.reset();
+        });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
       }
-
-      rt.corun_all();
     }
     // dynamic partitioner
     else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part,
-        [N, W, beg, &predicate, &offset, &next, &part] () mutable {
-          part.loop_until(N, W, next, 
-            [&, prev_e=size_t{0}](size_t curr_b, size_t curr_e) mutable {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop_until(N, W, *next, 
+            [=, &offset, prev_e=size_t{0}](size_t curr_b, size_t curr_e) mutable {
               return detail::find_if_not_loop(
-                offset, beg, prev_e, curr_b, curr_e, predicate
+                *offset, beg, prev_e, curr_b, curr_e, predicate
               );
             }
           );
-        }
-      );
+          // must release the ownership before async is destroyed
+          // as the node deletion comes after the join counter reaches zero
+          offset.reset();
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
     }
-
-    // update the result iterator by the offset
-    result = std::next(beg, offset.load(std::memory_order_relaxed));
   };
 }
 
 // Function: make_min_element_task
 template <typename B, typename E, typename T, typename C, typename P = DefaultPartitioner>
 auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
+  
+  using namespace std::string_literals;
 
   using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
   using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
@@ -227,17 +240,17 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
 
     // only myself - no need to spawn another graph
     if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        result = std::min_element(beg, end, comp);
-      });
+      part([&](){ result = std::min_element(beg, end, comp); })();
       return;
     }
 
+    PreemptionGuard preemption_guard(rt);
+
     if(N < W) {
       W = N;
     }
-
-    std::mutex mutex;
+    
+    auto mutex = std::make_shared<std::mutex>();
     
     // initialize the result to the first element
     result = beg++;
@@ -246,20 +259,17 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
     // static partitioner
     if constexpr(part.type() == PartitionerType::STATIC) {
       
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
         
         // we force chunk size to be at least two because the temporary
         // variable sum needs to avoid copy at the first step
-        chunk_size = std::max(size_t{2}, part.adjusted_chunk_size(N, W, w));
+        auto chunk_size = std::max(size_t{2}, part.adjusted_chunk_size(N, W, w));
         
-        launch_loop(W, w, rt, part,
-        [beg, curr_b, N, W, chunk_size, &comp, &mutex, &result, &part] () mutable {
+        auto task = part([=, &result] () mutable {
           std::advance(beg, curr_b);
 
           if(N - curr_b == 1) {
-            std::lock_guard<std::mutex> lock(mutex);
+            std::lock_guard<std::mutex> lock(*mutex);
             if(comp(*beg, *result)) {
               result = beg;
             }
@@ -272,7 +282,7 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
         
           // loop reduce
           part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=curr_b+2](size_t part_b, size_t part_e) mutable {
+            [=, &smallest, prev_e=curr_b+2](size_t part_b, size_t part_e) mutable {
 
               if(part_b > prev_e) {
                 std::advance(beg, part_b - prev_e);
@@ -291,21 +301,24 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
           ); 
           
           // final reduce
-          std::lock_guard<std::mutex> lock(mutex);
+          std::lock_guard<std::mutex> lock(*mutex);
           if(comp(*smallest, *result)) {
             result = smallest;
           }
         });
+        
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
       }
-      rt.corun_all();
     }
     // dynamic partitioner
     else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, 
-        [beg, N, W, &next, &comp, &mutex, &result, &part] () mutable {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      
+      for(size_t w=0; w<W;) {
+
+        auto task = part([=, &result] () mutable {
           // pre-reduce
-          size_t s0 = next.fetch_add(2, std::memory_order_relaxed);
+          size_t s0 = next->fetch_add(2, std::memory_order_relaxed);
 
           if(s0 >= N) {
             return;
@@ -314,7 +327,7 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
           std::advance(beg, s0);
 
           if(N - s0 == 1) {
-            std::lock_guard<std::mutex> lock(mutex);
+            std::lock_guard<std::mutex> lock(*mutex);
             if(comp(*beg, *result)) {
               result = beg;
             }
@@ -327,8 +340,8 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
           T smallest = comp(*beg1, *beg2) ? beg1 : beg2;
           
           // loop reduce
-          part.loop(N, W, next, 
-            [&, prev_e=s0+2](size_t part_b, size_t part_e) mutable {
+          part.loop(N, W, *next, 
+            [=, &smallest, prev_e=s0+2](size_t part_b, size_t part_e) mutable {
               std::advance(beg, part_b - prev_e);
               for(size_t x=part_b; x<part_e; x++, beg++) {
                 if(comp(*beg, *smallest)) {
@@ -340,12 +353,13 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
           ); 
           
           // final reduce
-          std::lock_guard<std::mutex> lock(mutex);
+          std::lock_guard<std::mutex> lock(*mutex);
           if(comp(*smallest, *result)) {
             result = smallest;
           }
-        }
-      );
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
     }
   };
 }
@@ -353,6 +367,8 @@ auto make_min_element_task(B first, E last, T& result, C comp, P part = P()) {
 // Function: make_max_element_task
 template <typename B, typename E, typename T, typename C, typename P = DefaultPartitioner>
 auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
+  
+  using namespace std::string_literals;
 
   using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
   using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
@@ -368,17 +384,17 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
 
     // only myself - no need to spawn another graph
     if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        result = std::max_element(beg, end, comp);
-      });
+      part([&](){ result = std::max_element(beg, end, comp); })();
       return;
     }
 
+    PreemptionGuard preemption_guard(rt);
+
     if(N < W) {
       W = N;
     }
 
-    std::mutex mutex;
+    auto mutex = std::make_shared<std::mutex>();
     
     // initialize the result to the first element
     result = beg++;
@@ -387,20 +403,18 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
     // static partitioner
     if constexpr(part.type() == PartitionerType::STATIC) {
       
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
         
         // we force chunk size to be at least two because the temporary
         // variable sum needs to avoid copy at the first step
-        chunk_size = std::max(size_t{2}, part.adjusted_chunk_size(N, W, w));
+        auto chunk_size = std::max(size_t{2}, part.adjusted_chunk_size(N, W, w));
         
-        launch_loop(W, w, rt, part,
-        [beg, curr_b, N, W, chunk_size, &comp, &mutex, &result, &part] () mutable {
+        auto task = part([=, &result] () mutable {
+
           std::advance(beg, curr_b);
 
           if(N - curr_b == 1) {
-            std::lock_guard<std::mutex> lock(mutex);
+            std::lock_guard<std::mutex> lock(*mutex);
             if(comp(*result, *beg)) {
               result = beg;
             }
@@ -413,7 +427,7 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
         
           // loop reduce
           part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=curr_b+2](size_t part_b, size_t part_e) mutable {
+            [=, &largest, prev_e=curr_b+2](size_t part_b, size_t part_e) mutable {
 
               if(part_b > prev_e) {
                 std::advance(beg, part_b - prev_e);
@@ -432,21 +446,23 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
           ); 
           
           // final reduce
-          std::lock_guard<std::mutex> lock(mutex);
+          std::lock_guard<std::mutex> lock(*mutex);
           if(comp(*result, *largest)) {
             result = largest;
           }
         });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
       }
-      rt.corun_all();
     }
     // dynamic partitioner
     else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part,
-        [beg, N, W, &next, &comp, &mutex, &result, &part] () mutable {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      
+      for(size_t w=0; w<W;) {
+
+        auto task = part([=, &result] () mutable {
           // pre-reduce
-          size_t s0 = next.fetch_add(2, std::memory_order_relaxed);
+          size_t s0 = next->fetch_add(2, std::memory_order_relaxed);
 
           if(s0 >= N) {
             return;
@@ -455,7 +471,7 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
           std::advance(beg, s0);
 
           if(N - s0 == 1) {
-            std::lock_guard<std::mutex> lock(mutex);
+            std::lock_guard<std::mutex> lock(*mutex);
             if(comp(*result, *beg)) {
               result = beg;
             }
@@ -468,8 +484,8 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
           T largest = comp(*beg1, *beg2) ? beg2 : beg1;
           
           // loop reduce
-          part.loop(N, W, next, 
-            [&, prev_e=s0+2](size_t part_b, size_t part_e) mutable {
+          part.loop(N, W, *next, 
+            [=, &largest, prev_e=s0+2](size_t part_b, size_t part_e) mutable {
               std::advance(beg, part_b - prev_e);
               for(size_t x=part_b; x<part_e; x++, beg++) {
                 if(comp(*largest, *beg)) {
@@ -481,18 +497,18 @@ auto make_max_element_task(B first, E last, T& result, C comp, P part = P()) {
           ); 
           
           // final reduce
-          std::lock_guard<std::mutex> lock(mutex);
+          std::lock_guard<std::mutex> lock(*mutex);
           if(comp(*result, *largest)) {
             result = largest;
           }
-        }
-      );
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
     }
   };
 }
 
 
-
 // Function: find_if
 template <typename B, typename E, typename T, typename UOP, typename P>
 Task tf::FlowBuilder::find_if(B first, E last, T& result, UOP predicate, P part) {
diff --git a/bundled/taskflow-3.10.0/taskflow/algorithm/for_each.hpp b/bundled/taskflow-3.10.0/taskflow/algorithm/for_each.hpp
new file mode 100644 (file)
index 0000000..376ff6f
--- /dev/null
@@ -0,0 +1,243 @@
+#pragma once
+
+#include "../taskflow.hpp"
+
+namespace tf {
+
+// Function: make_for_each_task
+template <typename B, typename E, typename C, typename P = DefaultPartitioner>
+auto make_for_each_task(B b, E e, C c, P part = P()) {
+  
+  using namespace std::string_literals;
+
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+
+  return [=] (Runtime& rt) mutable {
+
+    // fetch the stateful values
+    B_t beg = b;
+    E_t end = e;
+
+    size_t W = rt.executor().num_workers();
+    size_t N = std::distance(beg, end);
+
+    // the workload is sequentially doable
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([=](){ std::for_each(beg, end, c); })();
+      return;
+    }
+    
+    PreemptionGuard preemption_guard(rt);
+    
+    // use no more workers than the iteration count
+    if(N < W) {
+      W = N;
+    }
+    
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop(N, W, curr_b, chunk_size,
+            [=, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+              std::advance(beg, part_b - prev_e);
+              for(size_t x = part_b; x<part_e; x++) {
+                c(*beg++);
+              }
+              prev_e = part_e;
+            }
+          ); 
+        });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }
+    }
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop(N, W, *next, 
+            [=, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+              std::advance(beg, part_b - prev_e);
+              for(size_t x = part_b; x<part_e; x++) {
+                c(*beg++);
+              }
+              prev_e = part_e;
+            }
+          );
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }
+
+  };
+}
+
+// Function: make_for_each_index_task
+template <typename B, typename E, typename S, typename C, typename P = DefaultPartitioner>
+auto make_for_each_index_task(B b, E e, S s, C c, P part = P()){
+  
+  using namespace std::string_literals;
+
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using S_t = std::decay_t<unwrap_ref_decay_t<S>>;
+
+  return [=] (Runtime& rt) mutable {
+
+    // fetch the iterator values
+    B_t beg = b;
+    E_t end = e;
+    S_t inc = s;
+    
+    // nothing to be done if the range is invalid
+    if(is_index_range_invalid(beg, end, inc)) {
+      return;
+    }
+
+    size_t W = rt.executor().num_workers();
+    size_t N = distance(beg, end, inc);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([&](){
+        for(size_t x=0; x<N; x++, beg+=inc) {
+          c(beg);
+        }
+      })();
+      return;
+    }
+
+    PreemptionGuard preemption_guard(rt);
+    
+    if(N < W) {
+      W = N;
+    }
+    
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop(N, W, curr_b, chunk_size, [=] (size_t part_b, size_t part_e) {
+            auto idx = static_cast<B_t>(part_b) * inc + beg;
+            for(size_t x=part_b; x<part_e; x++, idx += inc) {
+              c(idx);
+            }
+          });
+        });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }
+    }
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop(N, W, *next, [=] (size_t part_b, size_t part_e) {
+            auto idx = static_cast<B_t>(part_b) * inc + beg;
+            for(size_t x=part_b; x<part_e; x++, idx += inc) {
+              c(idx);
+            }
+          });
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }
+  };
+}
+
+// Function: make_for_each_index_task
+template <typename R, typename C, typename P = DefaultPartitioner>
+auto make_for_each_index_task(R range, C c, P part = P()){
+  
+  using range_type = std::decay_t<unwrap_ref_decay_t<R>>;
+
+  return [=] (Runtime& rt) mutable {
+
+    // fetch the iterator values
+    range_type r = range;
+    
+    // nothing to be done if the range is invalid
+    if(is_index_range_invalid(r.begin(), r.end(), r.step_size())) {
+      return;
+    }
+
+    size_t W = rt.executor().num_workers();
+    size_t N = r.size();
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([&](){ c(r); })();
+      return;
+    }
+
+    PreemptionGuard preemption_guard(rt);
+    
+    if(N < W) {
+      W = N;
+    }
+    
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop(N, W, curr_b, chunk_size, [=] (size_t part_b, size_t part_e) {
+            c(r.discrete_domain(part_b, part_e));
+          });
+        });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }
+    }
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop(N, W, *next, [=] (size_t part_b, size_t part_e) {
+            c(r.discrete_domain(part_b, part_e));
+          });
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }
+  };
+}
+
+// ------------------------------------------------------------------------------------------------
+// for_each
+// ------------------------------------------------------------------------------------------------
+
+// Function: for_each
+template <typename B, typename E, typename C, typename P>
+Task FlowBuilder::for_each(B beg, E end, C c, P part) {
+  return emplace(
+    make_for_each_task(beg, end, c, part)
+  );
+}
+
+// ------------------------------------------------------------------------------------------------
+// for_each_index
+// ------------------------------------------------------------------------------------------------
+
+// Function: for_each_index
+template <typename B, typename E, typename S, typename C, typename P>
+Task FlowBuilder::for_each_index(B beg, E end, S inc, C c, P part){
+  return emplace(
+    make_for_each_index_task(beg, end, inc, c, part)
+  );
+}
+
+// Function: for_each_index
+template <typename R, typename C, typename P>
+Task FlowBuilder::for_each_index(R range, C c, P part){
+  return emplace(
+    make_for_each_index_task(range, c, part)
+  );
+}
+
+}  // end of namespace tf -------------------------------------------------------------------------
+
diff --git a/bundled/taskflow-3.10.0/taskflow/algorithm/module.hpp b/bundled/taskflow-3.10.0/taskflow/algorithm/module.hpp
new file mode 100644 (file)
index 0000000..566ac19
--- /dev/null
@@ -0,0 +1,81 @@
+#pragma once
+
+#include "../taskflow.hpp"
+
+namespace tf {
+
+// ----------------------------------------------------------------------------
+
+/**
+@private
+*/
+template <typename T>
+auto Algorithm::make_module_task(T&& target) {
+  return [&target=std::forward<T>(target)](tf::Runtime& rt){
+    auto& graph = target.graph();
+    if(graph.empty()) {
+      return;
+    }
+    PreemptionGuard preemption_guard(rt);
+    rt._executor._schedule_graph_with_parent(
+      rt._worker, graph.begin(), graph.end(), rt._parent, NSTATE::NONE
+    );
+  };
+}
+
+// ----------------------------------------------------------------------------
+
+/**
+ * @brief creates a module task using the given target
+ * 
+ * @tparam T Type of the target object, which must define the method `tf::Graph& graph()`.
+ * @param target The target object used to create the module task.
+ * @return module task that can be used by %Taskflow or asynchronous tasking.
+ * 
+ *
+ * This example demonstrates how to create and launch multiple taskflows in parallel 
+ * using asynchronous tasking:
+ *
+ * @code{.cpp}
+ * tf::Executor executor;
+ *
+ * tf::Taskflow A;
+ * tf::Taskflow B;
+ * tf::Taskflow C;
+ * tf::Taskflow D;
+ *
+ * A.emplace([](){ printf("Taskflow A\n"); }); 
+ * B.emplace([](){ printf("Taskflow B\n"); }); 
+ * C.emplace([](){ printf("Taskflow C\n"); }); 
+ * D.emplace([](){ printf("Taskflow D\n"); }); 
+ *
+ * // launch the four taskflows using asynchronous tasking
+ * executor.async(tf::make_module_task(A));
+ * executor.async(tf::make_module_task(B));
+ * executor.async(tf::make_module_task(C));
+ * executor.async(tf::make_module_task(D));
+ * executor.wait_for_all();  
+ * @endcode
+ *
+ * The module task maker, tf::make_module_task, is basically the same as tf::Taskflow::composed_of 
+ * but provides a more generic interface that can be used beyond %Taskflow.
+ * For instance, the following two approaches achieve the same functionality.
+ *
+ * @code{.cpp}
+ * // approach 1: composition using composed_of
+ * tf::Task m1 = taskflow1.composed_of(taskflow2);
+ * 
+ * // approach 2: composition using make_module_task
+ * tf::Task m1 = taskflow1.emplace(tf::make_module_task(taskflow2));
+ * @endcode
+ * 
+ * @attention
+ * Users are responsible for ensuring that the given target remains valid throughout its execution. 
+ * The executor does not assume ownership of the target object.
+ */
+template <typename T>
+auto make_module_task(T&& target) {
+  return Algorithm::make_module_task(std::forward<T>(target));
+}
+
+}  // end of namespact tf -----------------------------------------------------
similarity index 95%
rename from bundled/taskflow-3.7.0/taskflow/algorithm/partitioner.hpp
rename to bundled/taskflow-3.10.0/taskflow/algorithm/partitioner.hpp
index 04406f8343d0e465a31cf89a6376e378abdcd89e..b09952778e86d112d866f531772f577624febec1 100644 (file)
@@ -46,7 +46,7 @@ enum class PartitionerType : int {
 /**
 @struct DefaultClosureWrapper
 
-@brief default closure wrapper that simplies runs the given closure as is
+@brief default closure wrapper that simply runs the given closure as is
 */
 struct DefaultClosureWrapper {
 };
@@ -89,7 +89,7 @@ tf::GuidedPartitioner or tf::DynamicPartitioner can outperform tf::StaticPartiti
 In most situations, tf::GuidedPartitioner can deliver decent performance and
 is thus used as our default partitioner.
 
-@note
+@attention
 Giving the partition size of 0 lets the %Taskflow runtime automatically determines
 the partition size for the given partitioner.
 
@@ -121,7 +121,7 @@ taskflow.for_each_index(0, 100, 1,
 executor.run(taskflow).wait();
 @endcode
 
-@note
+@attention
 The default closure wrapper (tf::DefaultClosureWrapper) does nothing but invoke
 the partitioned task (closure).
 
@@ -131,6 +131,11 @@ class PartitionerBase : public IsPartitioner {
 
   public:
   
+  /**
+  @brief indicating if the given closure wrapper is a default wrapper (i.e., empty)
+  */
+  constexpr static bool is_default_wrapper_v = std::is_same_v<C, DefaultClosureWrapper>;
+  
   /** 
   @brief the closure type
   */
@@ -169,12 +174,31 @@ class PartitionerBase : public IsPartitioner {
   */
   const C& closure_wrapper() const { return _closure_wrapper; }
 
+  /**
+  @brief acquire a mutable access to the closure wrapper object
+  */
+  C& closure_wrapper() { return _closure_wrapper; }
+
   /**
   @brief modify the closure wrapper object
   */
   template <typename F>
   void closure_wrapper(F&& fn) { _closure_wrapper = std::forward<F>(fn); }
 
+  /**
+  @brief wraps the given callable with the associated closure wrapper
+  */
+  template <typename F>
+  TF_FORCE_INLINE decltype(auto) operator () (F&& callable) {
+    if constexpr(is_default_wrapper_v) {
+      return std::forward<F>(callable);
+    }
+    else {
+      // closure wrapper is stateful - capture it by reference
+      return [this, c=std::forward<F>(callable)]() mutable { _closure_wrapper(c); };
+    }
+  }
+
   protected:
   
   /**
@@ -790,8 +814,8 @@ class RandomPartitioner : public PartitionerBase<C> {
 /**
 @brief default partitioner set to tf::GuidedPartitioner
 
-Guided partitioner can achieve decent performance for most parallel algorithms,
-especially for those with irregular and unbalanced workload per iteration.
+Guided partitioning algorithm can achieve stable and decent performance
+for most parallel algorithms.
 */
 using DefaultPartitioner = GuidedPartitioner<>;
 
diff --git a/bundled/taskflow-3.10.0/taskflow/algorithm/reduce.hpp b/bundled/taskflow-3.10.0/taskflow/algorithm/reduce.hpp
new file mode 100644 (file)
index 0000000..e539617
--- /dev/null
@@ -0,0 +1,546 @@
+#pragma once
+
+#include "../taskflow.hpp"
+
+namespace tf {
+
+// Function: make_reduce_task
+template <typename B, typename E, typename T, typename O, typename P = DefaultPartitioner>
+auto make_reduce_task(B b, E e, T& init, O bop, P part = P()) {
+  
+  using namespace std::string_literals;
+
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+
+  return [=, &r=init] (Runtime& rt) mutable {
+
+    // fetch the iterator values
+    B_t beg = b;
+    E_t end = e;
+
+    size_t W = rt.executor().num_workers();
+    size_t N = std::distance(beg, end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([&](){ for(; beg!=end; r = bop(r, *beg++)); })();
+      return;
+    }
+    
+    PreemptionGuard preemption_guard(rt);
+
+    if(N < W) {
+      W = N;
+    }
+
+    auto mutex = std::make_shared<std::mutex>();
+
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+      
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        
+        // we force chunk size to be at least two because the temporary
+        // variable sum need to avoid copy at the first step
+        auto chunk_size = std::max(size_t{2}, part.adjusted_chunk_size(N, W, w));
+        
+        auto task = part([=, &r] () mutable {
+
+          std::advance(beg, curr_b);
+
+          if(N - curr_b == 1) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            r = bop(r, *beg);
+            return;
+          }
+
+          auto beg1 = beg++;
+          auto beg2 = beg++;
+          T sum = bop(*beg1, *beg2);
+        
+          // loop reduce
+          part.loop(N, W, curr_b, chunk_size,
+            [=, &sum, prev_e=curr_b+2](size_t part_b, size_t part_e) mutable {
+
+              if(part_b > prev_e) {
+                std::advance(beg, part_b - prev_e);
+              }
+              else {
+                part_b = prev_e;
+              }
+
+              for(size_t x=part_b; x<part_e; x++, beg++) {
+                sum = bop(sum, *beg);
+              }
+              prev_e = part_e;
+            }
+          ); 
+          
+          // final reduce
+          std::lock_guard<std::mutex> lock(*mutex);
+          r = bop(r, sum);
+        });
+
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }
+    }
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      
+      for(size_t w=0; w<W;) {
+
+        auto task = part([=, &r] () mutable {
+          // pre-reduce
+          size_t s0 = next->fetch_add(2, std::memory_order_relaxed);
+
+          if(s0 >= N) {
+            return;
+          }
+
+          std::advance(beg, s0);
+
+          if(N - s0 == 1) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            r = bop(r, *beg);
+            return;
+          }
+
+          auto beg1 = beg++;
+          auto beg2 = beg++;
+
+          T sum = bop(*beg1, *beg2);
+          
+          // loop reduce
+          part.loop(N, W, *next, 
+            [=, &sum, prev_e=s0+2](size_t curr_b, size_t curr_e) mutable {
+              std::advance(beg, curr_b - prev_e);
+              for(size_t x=curr_b; x<curr_e; x++, beg++) {
+                sum = bop(sum, *beg);
+              }
+              prev_e = curr_e;
+            }
+          ); 
+          
+          // final reduce
+          std::lock_guard<std::mutex> lock(*mutex);
+          r = bop(r, sum);
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }
+  };
+}
+
+// Function: make_transform_reduce_task
+template <
+  typename B, typename E, typename T, typename BOP, typename UOP, 
+  typename P = DefaultPartitioner
+>
+auto make_transform_reduce_task(B b, E e, T& init, BOP bop, UOP uop, P part = P()) {
+
+  using namespace std::string_literals;
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+
+  return [=, &r=init] (Runtime& rt) mutable {
+
+    // fetch the iterator values
+    B_t beg = b;
+    E_t end = e;
+
+    size_t W = rt.executor().num_workers();
+    size_t N = std::distance(beg, end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([&](){ for(; beg!=end; r = bop(std::move(r), uop(*beg++))); })();
+      return;
+    }
+    
+    PreemptionGuard preemption_guard(rt);
+
+    if(N < W) {
+      W = N;
+    }
+
+    auto mutex = std::make_shared<std::mutex>();
+    
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+      
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+
+        auto task = part([=, &r] () mutable {
+
+          std::advance(beg, curr_b);
+
+          if(N - curr_b == 1) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            r = bop(std::move(r), uop(*beg));
+            return;
+          }
+
+          //auto beg1 = beg++;
+          //auto beg2 = beg++;
+          //T sum = bop(uop(*beg1), uop(*beg2));
+
+          T sum = (chunk_size == 1) ? uop(*beg++) : bop(uop(*beg++), uop(*beg++));
+        
+          // loop reduce
+          part.loop(N, W, curr_b, chunk_size,
+            [=, &sum, prev_e=curr_b+(chunk_size == 1 ? 1 : 2)]
+            (size_t part_b, size_t part_e) mutable {
+              if(part_b > prev_e) {
+                std::advance(beg, part_b - prev_e);
+              }
+              else {
+                part_b = prev_e;
+              }
+              for(size_t x=part_b; x<part_e; x++, beg++) {
+                sum = bop(std::move(sum), uop(*beg));
+              }
+              prev_e = part_e;
+            }
+          ); 
+          
+          // final reduce
+          std::lock_guard<std::mutex> lock(*mutex);
+          r = bop(std::move(r), std::move(sum));
+        });
+
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }
+    }
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=, &r] () mutable {
+
+          // pre-reduce
+          size_t s0 = next->fetch_add(2, std::memory_order_relaxed);
+
+          if(s0 >= N) {
+            return;
+          }
+
+          std::advance(beg, s0);
+
+          if(N - s0 == 1) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            r = bop(std::move(r), uop(*beg));
+            return;
+          }
+
+          auto beg1 = beg++;
+          auto beg2 = beg++;
+
+          T sum = bop(uop(*beg1), uop(*beg2));
+          
+          // loop reduce
+          part.loop(N, W, *next, 
+            [=, &sum, prev_e=s0+2](size_t curr_b, size_t curr_e) mutable {
+              std::advance(beg, curr_b - prev_e);
+              for(size_t x=curr_b; x<curr_e; x++, beg++) {
+                sum = bop(std::move(sum), uop(*beg));
+              }
+              prev_e = curr_e;
+            }
+          ); 
+          
+          // final reduce
+          std::lock_guard<std::mutex> lock(*mutex);
+          r = bop(std::move(r), std::move(sum));
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }
+  };
+}
+
+// Function: make_transform_reduce_task with two binary operation
+template <
+  typename B1, typename E1, typename B2, typename T, typename BOP_R, typename BOP_T, 
+  typename P = DefaultPartitioner,
+  std::enable_if_t<!is_partitioner_v<std::decay_t<BOP_T>>, void>* = nullptr
+>
+auto make_transform_reduce_task(
+  B1 b1, E1 e1, B2 b2, T& init, BOP_R bop_r, BOP_T bop_t, P part = P()
+) {
+  
+  using namespace std::string_literals;
+
+  using B1_t = std::decay_t<unwrap_ref_decay_t<B1>>;
+  using E1_t = std::decay_t<unwrap_ref_decay_t<E1>>;
+  using B2_t = std::decay_t<unwrap_ref_decay_t<B2>>;
+
+  return [=, &r=init] (Runtime& rt) mutable {
+
+    // fetch the iterator values
+    B1_t beg1 = b1;
+    E1_t end1 = e1;
+    B2_t beg2 = b2; 
+
+    size_t W = rt.executor().num_workers();
+    size_t N = std::distance(beg1, end1);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([&](){ for(; beg1!=end1; r = bop_r(std::move(r), bop_t(*beg1++, *beg2++))); })();
+      return;
+    }   
+    
+    PreemptionGuard preemption_guard(rt);
+
+    if(N < W) {
+      W = N;
+    }   
+
+    auto mutex = std::make_shared<std::mutex>();
+    
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+    
+        auto chunk_size = part.adjusted_chunk_size(N, W, w); 
+
+        auto task = part([=, &r] () mutable {
+          std::advance(beg1, curr_b);
+          std::advance(beg2, curr_b);
+
+          if(N - curr_b == 1) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            r = bop_r(std::move(r), bop_t(*beg1, *beg2));
+            return;
+          }   
+
+          T sum = (chunk_size == 1) ? bop_t(*beg1++, *beg2++) : 
+            bop_r(bop_t(*beg1++, *beg2++), bop_t(*beg1++, *beg2++));
+    
+          // loop reduce
+          part.loop(N, W, curr_b, chunk_size,
+            [=, &sum, prev_e=curr_b+(chunk_size == 1 ? 1 : 2)] 
+            (size_t part_b, size_t part_e) mutable {
+              if(part_b > prev_e) {
+                std::advance(beg1, part_b - prev_e);
+                std::advance(beg2, part_b - prev_e);
+              }   
+              else {
+                part_b = prev_e;
+              }   
+              for(size_t x=part_b; x<part_e; x++, beg1++, beg2++) { 
+                sum = bop_r(std::move(sum), bop_t(*beg1, *beg2));
+              }   
+              prev_e = part_e;
+            }   
+          );  
+    
+          // final reduce
+          std::lock_guard<std::mutex> lock(*mutex);
+          r = bop_r(std::move(r), std::move(sum));
+        });
+
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }   
+    }   
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+    
+      for(size_t w=0; w<W;) {
+
+        auto task = part([=, &r] () mutable {
+
+          // pre-reduce
+          size_t s0 = next->fetch_add(2, std::memory_order_relaxed);
+
+          if(s0 >= N) {
+            return;
+          }   
+
+          std::advance(beg1, s0);
+          std::advance(beg2, s0);
+
+          if(N - s0 == 1) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            r = bop_r(std::move(r), bop_t(*beg1, *beg2));
+            return;
+          }   
+
+          auto beg11 = beg1++;
+          auto beg12 = beg1++;
+          auto beg21 = beg2++;
+          auto beg22 = beg2++;
+
+          T sum = bop_r(bop_t(*beg11, *beg21), bop_t(*beg12, *beg22));
+
+          // loop reduce
+          part.loop(N, W, *next, 
+            [=, &sum, prev_e=s0+2](size_t curr_b, size_t curr_e) mutable {
+              std::advance(beg1, curr_b - prev_e);
+              std::advance(beg2, curr_b - prev_e);
+              for(size_t x=curr_b; x<curr_e; x++, beg1++, beg2++) {
+                sum = bop_r(std::move(sum), bop_t(*beg1, *beg2));
+              }   
+              prev_e = curr_e;
+            }   
+          );  
+    
+          // final reduce
+          std::lock_guard<std::mutex> lock(*mutex);
+          r = bop_r(std::move(r), std::move(sum));
+        });
+        
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }   
+  };  
+}
+
+
+// Function: make_reduce_by_index_task
+template <typename R, typename T, typename L, typename G, typename P = DefaultPartitioner>
+auto make_reduce_by_index_task(R range, T& init, L lop, G gop, P part = P()) {
+  
+  using range_type = std::decay_t<unwrap_ref_decay_t<R>>;
+
+  return [=, &init] (Runtime& rt) mutable {
+
+    // fetch the iterator values
+    range_type r = range;
+    
+    // nothing to be done if the range is invalid
+    if(is_index_range_invalid(r.begin(), r.end(), r.step_size())) {
+      return;
+    }
+
+    size_t W = rt.executor().num_workers();
+    size_t N = r.size();
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= part.chunk_size()) {
+      part([&](){ init = lop(r, std::move(init)); })();
+      return;
+    }
+    
+    PreemptionGuard preemption_guard(rt);
+
+    if(N < W) {
+      W = N;
+    }
+
+    auto mutex = std::make_shared<std::mutex>();
+
+    // static partitioner
+    if constexpr(part.type() == PartitionerType::STATIC) {
+      
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        
+        // we force chunk size to be at least two because the temporary
+        // variable sum need to avoid copy at the first step
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        
+        auto task = part([=, &init] () mutable {
+
+          // temporary result so far
+          std::optional<T> tmp;
+
+          // loop reduce
+          part.loop(N, W, curr_b, chunk_size, [=, &tmp](size_t part_b, size_t part_e) mutable {
+              tmp = lop(r.discrete_domain(part_b, part_e), std::move(tmp));
+          }); 
+          
+          // final reduce - tmp is guaranteed to have value
+          // assert(tmp.has_value());
+          std::lock_guard<std::mutex> lock(*mutex);
+          init = gop(std::move(init), std::move(*tmp));
+        });
+
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
+      }
+    }
+    // dynamic partitioner
+    else {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      
+      for(size_t w=0; w<W;) {
+
+        auto task = part([=, &init] () mutable {
+          
+          // temporary result so far
+          std::optional<T> tmp;
+          
+          // loop reduce
+          part.loop(N, W, *next, [=, &tmp](size_t part_b, size_t part_e) mutable {
+            tmp = lop(r.discrete_domain(part_b, part_e), std::move(tmp));
+          }); 
+          
+          // final reduce - need to check if the running total has value since
+          // this is a dynamic scheduler; the worker may not actually acquire any work
+          if(tmp) {
+            std::lock_guard<std::mutex> lock(*mutex);
+            init = gop(std::move(init), std::move(*tmp));
+          }
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
+    }
+  };
+}
+
+// ------------------------------------------------------------------------------------------------
+// default reduction
+// ------------------------------------------------------------------------------------------------
+
+// Function: reduce
+template <typename B, typename E, typename T, typename O, typename P>
+Task FlowBuilder::reduce(B beg, E end, T& init, O bop, P part) {
+  return emplace(make_reduce_task(beg, end, init, bop, part));
+}
+
+// ------------------------------------------------------------------------------------------------
+// default transform and reduction
+// ------------------------------------------------------------------------------------------------
+
+// Function: transform_reduce
+template <typename B, typename E, typename T, typename BOP, typename UOP, typename P,
+  std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>*
+>
+Task FlowBuilder::transform_reduce(
+  B beg, E end, T& init, BOP bop, UOP uop, P part
+) {
+  return emplace(make_transform_reduce_task(beg, end, init, bop, uop, part));
+}
+
+// Function: transform_reduce
+template <
+  typename B1, typename E1, typename B2, typename T, typename BOP_R, typename BOP_T, 
+  typename P,
+  std::enable_if_t<!is_partitioner_v<std::decay_t<BOP_T>>, void>*
+>
+Task FlowBuilder::transform_reduce(
+  B1 beg1, E1 end1, B2 beg2, T& init, BOP_R bop_r, BOP_T bop_t, P part
+) {
+  return emplace(make_transform_reduce_task(beg1, end1, beg2, init, bop_r, bop_t, part));
+}
+
+// ------------------------------------------------------------------------------------------------
+// default reduce_by_key
+// ------------------------------------------------------------------------------------------------
+
+// Function: make_index_reduce_task
+template <typename R, typename T, typename L, typename G, typename P>
+Task FlowBuilder::reduce_by_index(R range, T& init, L lop, G gop, P part) {
+  return emplace(make_reduce_by_index_task(range, init, lop, gop, part));
+}
+
+}  // end of namespace tf -------------------------------------------------------------------------
+
+
+
+
diff --git a/bundled/taskflow-3.10.0/taskflow/algorithm/scan.hpp b/bundled/taskflow-3.10.0/taskflow/algorithm/scan.hpp
new file mode 100644 (file)
index 0000000..9573460
--- /dev/null
@@ -0,0 +1,652 @@
+#pragma once
+
+#include "../taskflow.hpp"
+
+namespace tf {
+
+/*
+
+Block-parallel scan algorithm:
+
+-----------------------------------------------------------------
+|    block 1    |    block 2    |    block 3    |    block 4    |
+-----------------------------------------------------------------
+
+                -----------------------------
+                |  B1  |  B2  |  B3  |  B4  |  // scan block sum to auxilinary array
+                -----------------------------
+                |                           |
+                v                           v
+                -----------------------------
+                |  B1  |  B2  |  B3  |  B4  |  // scan block sums
+                -----------------------------
+                   |
+                   |                           // add scanned block sum i to all 
+                   |                           // values of scanned block i+1
+                   v
+-----------------------------------------------------------------
+|    block 1    |    block 2    |    block 3    |    block 4    |
+-----------------------------------------------------------------
+
+Example OpenMP implementation for inclusive scan:
+
+void inclusive_scan(std::vector<int>& data) {
+
+  int n = data.size();
+  int num_threads;
+
+  #pragma omp parallel
+  {
+    num_threads = omp_get_num_threads();
+  }
+
+  std::vector<int> partial_sums(num_threads, 0);
+
+  // Step 1: Up-sweep
+  #pragma omp parallel
+  {
+    int tid = omp_get_thread_num();
+    int chunk_size = (n + num_threads - 1) / num_threads;
+    int start = tid * chunk_size;
+    int end = std::min(start + chunk_size, n);
+
+    // Compute partial sum
+    for (int i = start + 1; i < end; ++i) {
+      data[i] += data[i - 1];
+    }
+    partial_sums[tid] = data[end - 1];
+  }
+
+  // Step 2: Propagate partial sums
+  for (int i = 1; i < num_threads; ++i) {
+    partial_sums[i] += partial_sums[i - 1];
+  }
+
+  // Step 3: Down-sweep
+  #pragma omp parallel
+  {
+    int tid = omp_get_thread_num();
+    int chunk_size = (n + num_threads - 1) / num_threads;
+    int start = tid * chunk_size;
+    int end = std::min(start + chunk_size, n);
+
+    // Adjust with partial sums
+    if (tid > 0) {
+      for (int i = start; i < end; ++i) {
+        data[i] += partial_sums[tid - 1];
+      }
+    }
+  }
+}
+
+*/
+
+namespace detail {
+
+template <typename T>
+struct ScanData {
+
+  ScanData(size_t N, size_t c) : buf(N), counter(c) {}
+
+  std::vector<CachelineAligned<T>> buf;
+  std::atomic<size_t> counter;
+};
+
+// down scan task
+template <typename S, typename I, typename B>
+auto make_dscan_task(
+  std::shared_ptr<S> sdata, 
+  I d_beg,
+  B bop,
+  size_t w, 
+  size_t block_size
+) {
+  return [=, sdata=std::move(sdata)]() mutable {
+    for(size_t i=0; i<block_size; i++) {
+      *d_beg++ = bop(sdata->buf[w-1].data, *d_beg);
+    }
+  };
+}
+
+// middle scan task
+template <typename S, typename B>
+auto make_mscan_task(std::shared_ptr<S> sdata, B bop) {
+  return [=, sdata=std::move(sdata)](){
+    for(size_t i=1; i<sdata->buf.size(); i++) {
+      sdata->buf[i].data = bop(sdata->buf[i-1].data, sdata->buf[i].data);
+    }
+  };
+}
+
+}  // end of namespace tf::detail ---------------------------------------------
+
+
+// Function: make_inclusive_scan_task
+template <typename B, typename E, typename D, typename BOP>
+auto make_inclusive_scan_task(B first, E last, D d_first, BOP bop) {
+   
+  using namespace std::string_literals;
+  
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
+  using value_type = typename std::iterator_traits<B_t>::value_type;
+  
+  return [=] (Subflow& sf) mutable {
+
+    // fetch the stateful values
+    B_t s_beg = first;
+    E_t s_end = last;
+    D_t d_beg = d_first;
+
+    if(s_beg == s_end) {
+      return;
+    }
+
+    size_t W = sf.executor().num_workers();
+    size_t N = std::distance(s_beg, s_end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= 2) {
+      std::inclusive_scan(s_beg, s_end, d_beg, bop);
+      return;
+    }
+
+    if(N < W) {
+      W = N;
+    }
+    
+    auto sdata = std::make_shared< detail::ScanData<value_type> >(W, 0);
+
+    size_t Q = N/W;
+    size_t R = N%W;
+    
+    auto mscan = sf.emplace(make_mscan_task(sdata, bop)).name("mscan");
+    
+    for(size_t w=0; w<W; ++w) {
+      size_t block_size = Q + (w<R);
+
+      auto uscan = sf.emplace([=]() mutable {
+        auto& init = sdata->buf[w].data;
+        *d_beg++ = init = *s_beg++;
+        for(size_t i=1; i<block_size; i++){
+          *d_beg++ = init = bop(init, *s_beg++); 
+        }
+      }).name("uscan-"s + std::to_string(w));
+
+      uscan.precede(mscan);
+
+      if(w) {
+        sf.emplace(make_dscan_task(sdata, d_beg, bop, w, block_size))
+          .succeed(mscan)
+          .name("dscan-"s + std::to_string(w));
+      }
+
+      std::advance(s_beg, block_size);
+      std::advance(d_beg, block_size);
+    }
+  };
+}
+
+// Function: make_inclusive_scan_task
+template <typename B, typename E, typename D, typename BOP, typename T>
+auto make_inclusive_scan_task(B first, E last, D d_first, BOP bop, T init) {
+  
+  using namespace std::string_literals;
+
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
+  using value_type = typename std::iterator_traits<B_t>::value_type;
+  
+  return [=] (Subflow& sf) mutable {
+
+    // fetch the stateful values
+    B_t s_beg = first;
+    E_t s_end = last;
+    D_t d_beg = d_first;
+
+    if(s_beg == s_end) {
+      return;
+    }
+
+    size_t W = sf.executor().num_workers();
+    size_t N = std::distance(s_beg, s_end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= 2) {
+      std::inclusive_scan(s_beg, s_end, d_beg, bop, init);
+      return;
+    }
+
+    if(N < W) {
+      W = N;
+    }
+    
+    auto sdata = std::make_shared< detail::ScanData<value_type> >(W, 0);
+    
+    // set up the initial value for the first worker
+    sdata->buf[0].data = std::move(init);
+    
+    auto mscan = sf.emplace(make_mscan_task(sdata, bop)).name("mscan");
+
+    size_t Q = N/W;
+    size_t R = N%W;
+
+    for(size_t w=0; w<W; ++w) {
+
+      size_t block_size = Q + (w < R);
+
+      // block scan
+      auto uscan = sf.emplace([=] () mutable {
+        auto& local = sdata->buf[w].data;
+        *d_beg++ = local = (w == 0) ? bop(local, *s_beg++) : *s_beg++;
+        for(size_t i=1; i<block_size; i++){
+          *d_beg++ = local = bop(local, *s_beg++); 
+        }
+        //detail::scan_loop(rt, *sdata, bop, result, W, w, block_size);
+      }).name("uscan-"s + std::to_string(w));
+      
+      uscan.precede(mscan);
+      
+      if(w) {
+        sf.emplace(make_dscan_task(sdata, d_beg, bop, w, block_size))
+          .succeed(mscan)
+          .name("dscan-"s + std::to_string(w));
+      }
+
+      std::advance(s_beg, block_size);
+      std::advance(d_beg, block_size);
+    }
+  };
+}
+
+// ----------------------------------------------------------------------------
+// Transform Inclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: transform_inclusive_scan
+template <typename B, typename E, typename D, typename BOP, typename UOP>
+auto make_transform_inclusive_scan_task(
+  B first, E last, D d_first, BOP bop, UOP uop
+) {
+  
+  using namespace std::string_literals;
+
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
+  using value_type = typename std::iterator_traits<B_t>::value_type;
+  
+  return [=] (Subflow& sf) mutable {
+
+    // fetch the stateful values
+    B_t s_beg = first;
+    E_t s_end = last;
+    D_t d_beg = d_first;
+
+    if(s_beg == s_end) {
+      return;
+    }
+
+    size_t W = sf.executor().num_workers();
+    size_t N = std::distance(s_beg, s_end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= 2) {
+      std::transform_inclusive_scan(s_beg, s_end, d_beg, bop, uop);
+      return;
+    }
+    
+    if(N < W) {
+      W = N;
+    } 
+
+    auto sdata = std::make_shared< detail::ScanData<value_type> >(W, 0);
+
+    size_t Q = N/W;
+    size_t R = N%W;
+    
+    auto mscan = sf.emplace(make_mscan_task(sdata, bop)).name("mscan");
+    
+    for(size_t w=0; w<W; ++w) {
+
+      size_t block_size = Q + (w < R);
+
+      auto uscan = sf.emplace([=] () mutable {
+        auto& init = sdata->buf[w].data;
+        *d_beg++ = init = uop(*s_beg++);
+        for(size_t i=1; i<block_size; i++){
+          *d_beg++ = init = bop(init, uop(*s_beg++)); 
+        }
+        //detail::scan_loop(rt, *sdata, bop, result, W, w, block_size);
+      }).name("uscan-"s + std::to_string(w));
+      
+      uscan.precede(mscan);
+
+      if(w) {
+        sf.emplace(make_dscan_task(sdata, d_beg, bop, w, block_size))
+          .succeed(mscan)
+          .name("dscan-"s + std::to_string(w));
+      }
+      
+      std::advance(s_beg, block_size);
+      std::advance(d_beg, block_size);
+    }
+  };
+}
+
+// Function: transform_inclusive_scan
+template <typename B, typename E, typename D, typename BOP, typename UOP, typename T>
+auto make_transform_inclusive_scan_task(
+  B first, E last, D d_first, BOP bop, UOP uop, T init
+) {
+  
+  using namespace std::string_literals;
+
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
+  using value_type = typename std::iterator_traits<B_t>::value_type;
+  
+  return [=] (Subflow& sf) mutable {
+
+    // fetch the stateful values
+    B_t s_beg = first;
+    E_t s_end = last;
+    D_t d_beg = d_first;
+
+    if(s_beg == s_end) {
+      return;
+    }
+
+    size_t W = sf.executor().num_workers();
+    size_t N = std::distance(s_beg, s_end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= 2) {
+      std::transform_inclusive_scan(s_beg, s_end, d_beg, bop, uop, init);
+      return;
+    }
+
+    if(N < W) {
+      W = N;
+    }
+    
+    auto sdata = std::make_shared< detail::ScanData<value_type> >(W, 0);
+    
+    // set up the initial value for the first worker
+    sdata->buf[0].data = std::move(init);
+
+    size_t Q = N/W;
+    size_t R = N%W;
+    
+    auto mscan = sf.emplace(make_mscan_task(sdata, bop)).name("mscan");
+
+    for(size_t w=0; w<W; ++w) {
+
+      size_t block_size = Q + (w < R);
+
+      // block scan
+      auto uscan = sf.emplace([=]() mutable {
+        auto& local = sdata->buf[w].data;
+        *d_beg++ = local = (w == 0) ? bop(local, uop(*s_beg++)) : uop(*s_beg++);
+        for(size_t i=1; i<block_size; i++){
+          *d_beg++ = local = bop(local, uop(*s_beg++)); 
+        }
+        //detail::scan_loop(rt, *sdata, bop, result, W, w, block_size);
+      }).name("uscan-"s + std::to_string(w));
+
+      uscan.precede(mscan);
+
+      if(w) {
+        sf.emplace(make_dscan_task(sdata, d_beg, bop, w, block_size))
+          .succeed(mscan)
+          .name("dscan-"s + std::to_string(w));
+      }
+
+      std::advance(s_beg, block_size);
+      std::advance(d_beg, block_size);
+    }
+  };
+}
+
+// ----------------------------------------------------------------------------
+// Exclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: make_exclusive_scan_task
+template <typename B, typename E, typename D, typename T, typename BOP>
+auto make_exclusive_scan_task(
+  B first, E last, D d_first, T init, BOP bop
+) {
+
+  using namespace std::string_literals;
+  
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
+  using value_type = typename std::iterator_traits<B_t>::value_type;
+  
+  return [=] (Subflow& sf) mutable {
+
+    // fetch the stateful values
+    B_t s_beg = first;
+    E_t s_end = last;
+    D_t d_beg = d_first;
+
+    if(s_beg == s_end) {
+      return;
+    }
+
+    size_t W = sf.executor().num_workers();
+    size_t N = std::distance(s_beg, s_end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= 2) {
+      std::exclusive_scan(s_beg, s_end, d_beg, init, bop);
+      return;
+    }
+
+    if(N < W) {
+      W = N;
+    }
+    
+    auto sdata = std::make_shared< detail::ScanData<value_type> >(W, 0);
+
+    size_t Q = N/W;
+    size_t R = N%W;
+
+    auto mscan = sf.emplace(make_mscan_task(sdata, bop)).name("mscan");
+
+    // fetch the init value
+    auto s_beg_temp = s_beg;
+    for(size_t w=0; w<W; ++w) {
+      size_t block_size = Q + (w<R);  
+      sdata->buf[w].data = w ? *s_beg_temp : std::move(init);
+      std::advance(s_beg_temp, block_size - !w);
+    }
+    
+    for(size_t w=0; w<W; ++w) {
+
+      size_t block_size = (Q + (w < R));
+
+      auto uscan = sf.emplace([=] () mutable {
+        auto& local = sdata->buf[w].data;
+        for(size_t i=1; i<block_size; i++) {
+          auto v = local;
+          local = bop(local, *s_beg++);
+          *d_beg++ = std::move(v);
+        }
+        *d_beg++ = local;
+        //detail::scan_loop(rt, *sdata, bop, result, W, w, block_size);
+      }).name("uscan-"s + std::to_string(w));
+      
+      uscan.precede(mscan);
+
+      if(w) {
+        sf.emplace(make_dscan_task(sdata, d_beg, bop, w, block_size))
+          .succeed(mscan)
+          .name("dscan-"s + std::to_string(w));
+      }
+      
+      std::advance(s_beg, block_size);
+      std::advance(d_beg, block_size);
+    }
+
+  };
+}
+
+// ----------------------------------------------------------------------------
+// Transform Exclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: 
+template <typename B, typename E, typename D, typename T, typename BOP, typename UOP>
+auto make_transform_exclusive_scan_task(
+  B first, E last, D d_first, T init, BOP bop, UOP uop
+) {
+
+  using namespace std::string_literals;
+  
+  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
+  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
+  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
+  using value_type = typename std::iterator_traits<B_t>::value_type;
+  
+  return [=] (Subflow& sf) mutable {
+
+    // fetch the stateful values
+    B_t s_beg = first;
+    E_t s_end = last;
+    D_t d_beg = d_first;
+
+    if(s_beg == s_end) {
+      return;
+    }
+
+    size_t W = sf.executor().num_workers();
+    size_t N = std::distance(s_beg, s_end);
+
+    // only myself - no need to spawn another graph
+    if(W <= 1 || N <= 2) {
+      std::transform_exclusive_scan(s_beg, s_end, d_beg, init, bop, uop);
+      return;
+    }
+
+    if(N < W) {
+      W = N;
+    }
+    
+    auto sdata = std::make_shared< detail::ScanData<value_type> >(W, 0);
+    
+    size_t Q = N/W;
+    size_t R = N%W;
+    
+    auto mscan = sf.emplace(make_mscan_task(sdata, bop)).name("mscan");
+
+    // fetch the init value
+    auto s_beg_temp = s_beg;
+    for(size_t w=0; w<W; ++w) {
+      size_t block_size = Q + (w < R);
+      sdata->buf[w].data = w ? uop(*s_beg_temp) : std::move(init);
+      std::advance(s_beg_temp, block_size - !w);
+    }
+    
+    for(size_t w=0; w<W; ++w) {
+
+      size_t block_size = Q + (w < R);
+
+      // block scan
+      auto uscan = sf.emplace([=]() mutable {
+        auto& local = sdata->buf[w].data;
+        for(size_t i=1; i<block_size; i++) {
+          auto v = local;
+          local = bop(local, uop(*s_beg++));
+          *d_beg++ = std::move(v);
+        }
+        *d_beg++ = local;
+        //detail::scan_loop(rt, *sdata, bop, result, W, w, block_size);
+      }).name("uscan-"s + std::to_string(w));
+      
+      uscan.precede(mscan);
+
+      if(w) {
+        sf.emplace(make_dscan_task(sdata, d_beg, bop, w, block_size))
+          .succeed(mscan)
+          .name("dscan-"s + std::to_string(w));
+      }
+      
+      std::advance(s_beg, block_size);
+      std::advance(d_beg, block_size);
+    } 
+  };
+}
+
+
+// ----------------------------------------------------------------------------
+// Inclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: inclusive_scan
+template <typename B, typename E, typename D, typename BOP>
+Task FlowBuilder::inclusive_scan(B first, E last, D d_first, BOP bop) {
+  return emplace(make_inclusive_scan_task(first, last, d_first, bop));
+}
+
+// Function: inclusive_scan
+template <typename B, typename E, typename D, typename BOP, typename T>
+Task FlowBuilder::inclusive_scan(B first, E last, D d_first, BOP bop, T init) {
+  return emplace(make_inclusive_scan_task(first, last, d_first, bop, init));
+}
+
+// ----------------------------------------------------------------------------
+// Transform Inclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: transform_inclusive_scan
+template <typename B, typename E, typename D, typename BOP, typename UOP>
+Task FlowBuilder::transform_inclusive_scan(
+  B first, E last, D d_first, BOP bop, UOP uop
+) {
+  return emplace(make_transform_inclusive_scan_task(
+    first, last, d_first, bop, uop
+  ));
+}
+
+// Function: transform_inclusive_scan
+template <typename B, typename E, typename D, typename BOP, typename UOP, typename T>
+Task FlowBuilder::transform_inclusive_scan(
+  B first, E last, D d_first, BOP bop, UOP uop, T init
+) {
+  return emplace(make_transform_inclusive_scan_task(
+    first, last, d_first, bop, uop, init
+  ));  
+}
+
+// ----------------------------------------------------------------------------
+// Exclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: exclusive_scan
+template <typename B, typename E, typename D, typename T, typename BOP>
+Task FlowBuilder::exclusive_scan(B first, E last, D d_first, T init, BOP bop) {
+  return emplace(make_exclusive_scan_task(first, last, d_first, init, bop));
+}
+
+// ----------------------------------------------------------------------------
+// Transform Exclusive Scan
+// ----------------------------------------------------------------------------
+
+// Function: transform_exclusive_scan
+template <typename B, typename E, typename D, typename T, typename BOP, typename UOP>
+Task FlowBuilder::transform_exclusive_scan(
+  B first, E last, D d_first, T init, BOP bop, UOP uop
+) {
+  return emplace(make_transform_exclusive_scan_task(
+    first, last, d_first, init, bop, uop
+  )); 
+}
+
+}  // end of namespace tf -----------------------------------------------------
+
similarity index 94%
rename from bundled/taskflow-3.7.0/taskflow/algorithm/sort.hpp
rename to bundled/taskflow-3.10.0/taskflow/algorithm/sort.hpp
index 4460f8f4a3613a169cbace86635d228de40c0543..1967516bb7b69b366057d4fa24e18d5bac6bd8da 100644 (file)
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../core/async.hpp"
+#include "../taskflow.hpp"
 
 namespace tf::detail {
 
@@ -39,7 +39,8 @@ inline T* align_cacheline(T* p) {
 #else
   std::size_t ip = reinterpret_cast<std::size_t>(p);
 #endif
-  ip = (ip + cacheline_size - 1) & -cacheline_size;
+  //ip = (ip + cacheline_size - 1) & -cacheline_size;
+  ip = (ip + cacheline_size - 1) & ~(cacheline_size - 1);
   return reinterpret_cast<T*>(ip);
 }
 
@@ -224,7 +225,7 @@ std::pair<Iter, bool> partition_right_branchless(Iter begin, Iter end, Compare c
 
       // Fill the offset blocks.
       if (left_split >= block_size) {
-        for (size_t i = 0; i < block_size;) {
+        for (unsigned char i = 0; i < block_size;) {
           offsets_l[num_l] = i++; num_l += !comp(*first, pivot); ++first;
           offsets_l[num_l] = i++; num_l += !comp(*first, pivot); ++first;
           offsets_l[num_l] = i++; num_l += !comp(*first, pivot); ++first;
@@ -235,13 +236,13 @@ std::pair<Iter, bool> partition_right_branchless(Iter begin, Iter end, Compare c
           offsets_l[num_l] = i++; num_l += !comp(*first, pivot); ++first;
         }
       } else {
-        for (size_t i = 0; i < left_split;) {
+        for (unsigned char i = 0; i < left_split;) {
           offsets_l[num_l] = i++; num_l += !comp(*first, pivot); ++first;
         }
       }
 
       if (right_split >= block_size) {
-        for (size_t i = 0; i < block_size;) {
+        for (unsigned char i = 0; i < block_size;) {
           offsets_r[num_r] = ++i; num_r += comp(*--last, pivot);
           offsets_r[num_r] = ++i; num_r += comp(*--last, pivot);
           offsets_r[num_r] = ++i; num_r += comp(*--last, pivot);
@@ -252,7 +253,7 @@ std::pair<Iter, bool> partition_right_branchless(Iter begin, Iter end, Compare c
           offsets_r[num_r] = ++i; num_r += comp(*--last, pivot);
         }
       } else {
-        for (size_t i = 0; i < right_split;) {
+        for (unsigned char i = 0; i < right_split;) {
           offsets_r[num_r] = ++i; num_r += comp(*--last, pivot);
         }
       }
@@ -387,7 +388,7 @@ RandItr partition_left(RandItr begin, RandItr end, Compare comp) {
 
 template<typename Iter, typename Compare, bool Branchless>
 void parallel_pdqsort(
-  tf::Runtime& rt,
+  Runtime& rt,
   Iter begin, Iter end, Compare comp,
   int bad_allowed, bool leftmost = true
 ) {
@@ -512,13 +513,12 @@ void parallel_pdqsort(
 
     // Sort the left partition first using recursion and
     // do tail recursion elimination for the right-hand partition.
-    rt.silent_async(
-      [&rt, begin, pivot_pos, comp, bad_allowed, leftmost] () mutable {
-        parallel_pdqsort<Iter, Compare, Branchless>(
-          rt, begin, pivot_pos, comp, bad_allowed, leftmost
-        );
-      }
-    );
+    // here we need to copy runtime so it stays alive during the sort recursion
+    rt.silent_async([=] () mutable {
+      parallel_pdqsort<Iter, Compare, Branchless>(
+        rt, begin, pivot_pos, comp, bad_allowed, leftmost
+      );
+    });
     begin = pivot_pos + 1;
     leftmost = false;
   }
@@ -530,7 +530,7 @@ void parallel_pdqsort(
 
 // 3-way quick sort
 template <typename RandItr, typename C>
-void parallel_3wqsort(tf::Runtime& rt, RandItr first, RandItr last, C compare) {
+void parallel_3wqsort(Runtime& rt, RandItr first, RandItr last, C compare) {
 
   using namespace std::string_literals;
 
@@ -573,26 +573,15 @@ void parallel_3wqsort(tf::Runtime& rt, RandItr first, RandItr last, C compare) {
   }
 
   if(l - first > 1 && is_swapped_l) {
-    //rt.emplace([&](tf::Runtime& rtl) mutable {
-    //  parallel_3wqsort(rtl, first, l-1, compare);
-    //});
-    rt.silent_async([&rt, first, l, &compare] () mutable {
+    rt.silent_async([=] () mutable {
       parallel_3wqsort(rt, first, l-1, compare);
     });
   }
 
   if(last - r > 1 && is_swapped_r) {
-    //rt.emplace([&](tf::Runtime& rtr) mutable {
-    //  parallel_3wqsort(rtr, r+1, last, compare);
-    //});
-    //rt.silent_async([&rt, r, last, &compare] () mutable {
-    //  parallel_3wqsort(rt, r+1, last, compare);
-    //});
     first = r+1;
     goto sort_partition;
   }
-
-  //rt.join();
 }
 
 }  // end of namespace tf::detail ---------------------------------------------
@@ -601,7 +590,7 @@ namespace tf {
 
 // Function: make_sort_task
 template <typename B, typename E, typename C>
-TF_FORCE_INLINE auto make_sort_task(B b, E e, C cmp) {
+auto make_sort_task(B b, E e, C cmp) {
   
   return [b, e, cmp] (Runtime& rt) mutable {
 
@@ -625,18 +614,18 @@ TF_FORCE_INLINE auto make_sort_task(B b, E e, C cmp) {
       return;
     }
 
-    //parallel_3wqsort(rt, beg, end-1, cmp);
+    PreemptionGuard preemption_guard(rt);
+
+    //detail::parallel_3wqsort(rt, beg, end-1, cmp);
     detail::parallel_pdqsort<B_t, C,
       is_std_compare_v<std::decay_t<C>> &&
       std::is_arithmetic_v<typename std::iterator_traits<B_t>::value_type>
     >(rt, beg, end, cmp, log2(end - beg));
-
-    rt.corun_all();
   };
 }
   
 template <typename B, typename E>
-TF_FORCE_INLINE auto make_sort_task(B beg, E end) {
+auto make_sort_task(B beg, E end) {
   using value_type = std::decay_t<decltype(*std::declval<B>())>;
   return make_sort_task(beg, end, std::less<value_type>{});
 }
similarity index 63%
rename from bundled/taskflow-3.7.0/taskflow/algorithm/transform.hpp
rename to bundled/taskflow-3.10.0/taskflow/algorithm/transform.hpp
index b155f658bcdbf0e9f496e1b07c7d184c8e7b6841..0ce692276b3c136d03075dcc405b0b850cbd0ec5 100644 (file)
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "launch.hpp"
+#include "../taskflow.hpp"
 
 namespace tf {
 
@@ -10,6 +10,8 @@ template <
   std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>* = nullptr
 >
 auto make_transform_task(B first1, E last1, O d_first, C c, P part = P()) {
+  
+  using namespace std::string_literals;
 
   using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
   using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
@@ -27,51 +29,49 @@ auto make_transform_task(B first1, E last1, O d_first, C c, P part = P()) {
 
     // only myself - no need to spawn another graph
     if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        std::transform(beg, end, d_beg, c);
-      });
+      part([=](){ std::transform(beg, end, d_beg, c); })();
       return;
     }
 
+    PreemptionGuard preemption_guard(rt);
+    
     if(N < W) {
       W = N;
     }
 
     // static partitioner
     if constexpr(part.type() == PartitionerType::STATIC) {
-      size_t chunk_size;
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-        launch_loop(W, w, rt, part, [=, &part] () mutable {
-          part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
-              std::advance(beg, part_b - prev_e);
-              std::advance(d_beg, part_b - prev_e);
-              for(size_t x = part_b; x<part_e; x++) {
-                *d_beg++ = c(*beg++);
-              }
-              prev_e = part_e;
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop(N, W, curr_b, chunk_size, [=, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+            std::advance(beg, part_b - prev_e);
+            std::advance(d_beg, part_b - prev_e);
+            for(size_t x = part_b; x<part_e; x++) {
+              *d_beg++ = c(*beg++);
             }
-          );
+            prev_e = part_e;
+          });
         });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
       }
-      rt.corun_all();
     }
     // dynamic partitioner
     else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, [=, &next, &part] () mutable {
-        part.loop(N, W, next, 
-          [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop(N, W, *next, [=, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
             std::advance(beg, part_b - prev_e);
             std::advance(d_beg, part_b - prev_e);
             for(size_t x = part_b; x<part_e; x++) {
               *d_beg++ = c(*beg++);
             }
             prev_e = part_e;
-          }
-        ); 
-      });
+          }); 
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
     }
   };
 }
@@ -82,6 +82,8 @@ template <
   std::enable_if_t<!is_partitioner_v<std::decay_t<C>>, void>* = nullptr
 >
 auto make_transform_task(B1 first1, E1 last1, B2 first2, O d_first, C c, P part = P()) {
+  
+  using namespace std::string_literals;
 
   using B1_t = std::decay_t<unwrap_ref_decay_t<B1>>;
   using E1_t = std::decay_t<unwrap_ref_decay_t<E1>>;
@@ -101,11 +103,11 @@ auto make_transform_task(B1 first1, E1 last1, B2 first2, O d_first, C c, P part
 
     // only myself - no need to spawn another graph
     if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        std::transform(beg1, end1, beg2, d_beg, c);
-      });
+      part([=](){ std::transform(beg1, end1, beg2, d_beg, c); })();
       return;
     }
+    
+    PreemptionGuard preemption_guard(rt);
 
     if(N < W) {
       W = N;
@@ -113,31 +115,28 @@ auto make_transform_task(B1 first1, E1 last1, B2 first2, O d_first, C c, P part
 
     // static partitioner
     if constexpr(part.type() == PartitionerType::STATIC) {
-      size_t chunk_size;
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-        launch_loop(W, w, rt, part, [=, &c, &part] () mutable {
-          part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
-              std::advance(beg1, part_b - prev_e);
-              std::advance(beg2, part_b - prev_e);
-              std::advance(d_beg, part_b - prev_e);
-              for(size_t x = part_b; x<part_e; x++) {
-                *d_beg++ = c(*beg1++, *beg2++);
-              }
-              prev_e = part_e;
+      for(size_t w=0, curr_b=0; w<W && curr_b < N;) {
+        auto chunk_size = part.adjusted_chunk_size(N, W, w);
+        auto task = part([=] () mutable {
+          part.loop(N, W, curr_b, chunk_size, [=, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+            std::advance(beg1, part_b - prev_e);
+            std::advance(beg2, part_b - prev_e);
+            std::advance(d_beg, part_b - prev_e);
+            for(size_t x = part_b; x<part_e; x++) {
+              *d_beg++ = c(*beg1++, *beg2++);
             }
-          );
+            prev_e = part_e;
+          });
         });
+        (++w == W || (curr_b += chunk_size) >= N) ? task() : rt.silent_async(task);
       }
-      rt.corun_all();
     }
     // dynamic partitioner
     else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, [=, &c, &next, &part] () mutable {
-        part.loop(N, W, next, 
-          [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
+      auto next = std::make_shared<std::atomic<size_t>>(0);
+      for(size_t w=0; w<W;) {
+        auto task = part([=] () mutable {
+          part.loop(N, W, *next, [=, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
             std::advance(beg1, part_b - prev_e);
             std::advance(beg2, part_b - prev_e);
             std::advance(d_beg, part_b - prev_e);
@@ -145,9 +144,10 @@ auto make_transform_task(B1 first1, E1 last1, B2 first2, O d_first, C c, P part
               *d_beg++ = c(*beg1++, *beg2++);
             }
             prev_e = part_e;
-          }
-        );
-      });
+          });
+        });
+        (++w == W) ? task() : rt.silent_async(task);
+      }
     }
   };
 }
similarity index 66%
rename from bundled/taskflow-3.7.0/taskflow/core/async.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/async.hpp
index ed671f9208b783ffbe2c40da7790f2855917fd40..4e4914406db2af83e81f3e47186f6368bc8b1038 100644 (file)
@@ -13,24 +13,8 @@ namespace tf {
 // Function: async
 template <typename P, typename F>
 auto Executor::async(P&& params, F&& f) {
-
   _increment_topology();
-
-  using R = std::invoke_result_t<std::decay_t<F>>;
-
-  std::packaged_task<R()> p(std::forward<F>(f));
-  auto fu{p.get_future()};
-
-  auto node = node_pool.animate(
-    std::forward<P>(params), nullptr, nullptr, 0, 
-    // handle
-    std::in_place_type_t<Node::Async>{}, 
-    [p=make_moc(std::move(p))]() mutable { p.object(); }
-  );
-
-  _schedule_async_task(node);
-
-  return fu;
+  return _async(std::forward<P>(params), std::forward<F>(f), nullptr, nullptr);
 }
 
 // Function: async
@@ -46,16 +30,10 @@ auto Executor::async(F&& f) {
 // Function: silent_async
 template <typename P, typename F>
 void Executor::silent_async(P&& params, F&& f) {
-
   _increment_topology();
-  
-  auto node = node_pool.animate(
-    std::forward<P>(params), nullptr, nullptr, 0, 
-    // handle
-    std::in_place_type_t<Node::Async>{}, std::forward<F>(f)
+  _silent_async(
+    std::forward<P>(params), std::forward<F>(f), nullptr, nullptr
   );
-
-  _schedule_async_task(node);
 }
 
 // Function: silent_async
@@ -70,25 +48,29 @@ void Executor::silent_async(F&& f) {
 
 // Procedure: _schedule_async_task
 inline void Executor::_schedule_async_task(Node* node) {  
-  if(auto w = _this_worker(); w) {
-    _schedule(*w, node);
-  }
-  else{
-    _schedule(node);
-  }
+  // Here we don't use _this_worker since _schedule will check if the
+  // given worker belongs to this executor.
+  (pt::this_worker && pt::this_worker->_executor == this) ? _schedule(*pt::this_worker, node) : 
+                                                            _schedule(node);
 }
 
 // Procedure: _tear_down_async
-inline void Executor::_tear_down_async(Node* node) {
-  // from runtime
-  if(node->_parent) {
-    node->_parent->_join_counter.fetch_sub(1, std::memory_order_release);
-  }
+inline void Executor::_tear_down_async(Worker& worker, Node* node, Node*& cache) {
+  
   // from executor
-  else {
+  if(auto parent = node->_parent; parent == nullptr) {
     _decrement_topology();
   }
-  node_pool.recycle(node);
+  // from runtime
+  else {
+    auto state = parent->_nstate;
+    if(parent->_join_counter.fetch_sub(1, std::memory_order_acq_rel) == 1) {
+      if(state & NSTATE::PREEMPTED) {
+        _update_cache(worker, cache, parent);
+      }
+    }
+  }
+  recycle(node);
 }
 
 // ----------------------------------------------------------------------------
@@ -112,26 +94,10 @@ template <typename P, typename F, typename... Tasks,
 tf::AsyncTask Executor::silent_dependent_async(
   P&& params, F&& func, Tasks&&... tasks 
 ){
-
-  _increment_topology();
-
-  size_t num_dependents = sizeof...(Tasks);
-  
-  // create a task before scheduling the node to retain a shared ownership first
-  AsyncTask task(node_pool.animate(
-    std::forward<P>(params), nullptr, nullptr, num_dependents,
-    std::in_place_type_t<Node::DependentAsync>{}, std::forward<F>(func)
-  ));
-  
-  if constexpr(sizeof...(Tasks) > 0) {
-    (_process_async_dependent(task._node, tasks, num_dependents), ...);
-  }
-
-  if(num_dependents == 0) {
-    _schedule_async_task(task._node);
-  }
-
-  return task;
+  std::array<AsyncTask, sizeof...(Tasks)> array = { std::forward<Tasks>(tasks)... };
+  return silent_dependent_async(
+    std::forward<P>(params), std::forward<F>(func), array.begin(), array.end()
+  );
 }
 
 // Function: silent_dependent_async
@@ -154,12 +120,12 @@ tf::AsyncTask Executor::silent_dependent_async(
 
   size_t num_dependents = std::distance(first, last);
   
-  AsyncTask task(node_pool.animate(
+  AsyncTask task(animate(
     std::forward<P>(params), nullptr, nullptr, num_dependents,
     std::in_place_type_t<Node::DependentAsync>{}, std::forward<F>(func)
   ));
   
-  for(; first != last; first++){
+  for(; first != last; first++) {
     _process_async_dependent(task._node, *first, num_dependents);
   }
 
@@ -187,31 +153,10 @@ template <typename P, typename F, typename... Tasks,
   std::enable_if_t<is_task_params_v<P> && all_same_v<AsyncTask, std::decay_t<Tasks>...>, void>*
 >
 auto Executor::dependent_async(P&& params, F&& func, Tasks&&... tasks) {
-  
-  _increment_topology();
-  
-  using R = std::invoke_result_t<std::decay_t<F>>;
-
-  std::packaged_task<R()> p(std::forward<F>(func));
-  auto fu{p.get_future()};
-
-  size_t num_dependents = sizeof...(tasks);
-
-  AsyncTask task(node_pool.animate(
-    std::forward<P>(params), nullptr, nullptr, num_dependents,
-    std::in_place_type_t<Node::DependentAsync>{},
-    [p=make_moc(std::move(p))] () mutable { p.object(); }
-  ));
-  
-  if constexpr(sizeof...(Tasks) > 0) {
-    (_process_async_dependent(task._node, tasks, num_dependents), ...);
-  }
-
-  if(num_dependents == 0) {
-    _schedule_async_task(task._node);
-  }
-
-  return std::make_pair(std::move(task), std::move(fu));
+  std::array<AsyncTask, sizeof...(Tasks)> array = { std::forward<Tasks>(tasks)... };
+  return dependent_async(
+    std::forward<P>(params), std::forward<F>(func), array.begin(), array.end()
+  );
 }
 
 // Function: dependent_async
@@ -229,29 +174,64 @@ template <typename P, typename F, typename I,
 auto Executor::dependent_async(P&& params, F&& func, I first, I last) {
   
   _increment_topology();
+    
+  size_t num_dependents = std::distance(first, last);
   
-  using R = std::invoke_result_t<std::decay_t<F>>;
-
-  std::packaged_task<R()> p(std::forward<F>(func));
-  auto fu{p.get_future()};
+  // async with runtime: [] (tf::Runtime&) {}
+  if constexpr (is_runtime_task_v<F>) {
+
+    std::promise<void> p;
+    auto fu{p.get_future()};
+
+    AsyncTask task(animate(
+      NSTATE::NONE, ESTATE::ANCHORED, std::forward<P>(params), nullptr, nullptr, num_dependents,
+      std::in_place_type_t<Node::DependentAsync>{},
+      [p=MoC{std::move(p)}, f=std::forward<F>(func)] (tf::Runtime& rt, bool reentered) mutable { 
+        if(!reentered) {
+          f(rt); 
+        }
+        else {
+          auto& eptr = rt._parent->_exception_ptr;
+          eptr ? p.object.set_exception(eptr) : p.object.set_value();
+        }
+      }
+    ));
 
-  size_t num_dependents = std::distance(first, last);
+    for(; first != last; first++) {
+      _process_async_dependent(task._node, *first, num_dependents);
+    }
 
-  AsyncTask task(node_pool.animate(
-    std::forward<P>(params), nullptr, nullptr, num_dependents,
-    std::in_place_type_t<Node::DependentAsync>{},
-    [p=make_moc(std::move(p))] () mutable { p.object(); }
-  ));
+    if(num_dependents == 0) {
+      _schedule_async_task(task._node);
+    }
 
-  for(; first != last; first++) {
-    _process_async_dependent(task._node, *first, num_dependents);
+    return std::make_pair(std::move(task), std::move(fu));
   }
+  // async without runtime: [] () {}
+  else if constexpr(std::is_invocable_v<F>) {
 
-  if(num_dependents == 0) {
-    _schedule_async_task(task._node);
-  }
+    std::packaged_task p(std::forward<F>(func));
+    auto fu{p.get_future()};
+
+    AsyncTask task(animate(
+      std::forward<P>(params), nullptr, nullptr, num_dependents,
+      std::in_place_type_t<Node::DependentAsync>{},
+      [p=make_moc(std::move(p))] () mutable { p.object(); }
+    ));
 
-  return std::make_pair(std::move(task), std::move(fu));
+    for(; first != last; first++) {
+      _process_async_dependent(task._node, *first, num_dependents);
+    }
+
+    if(num_dependents == 0) {
+      _schedule_async_task(task._node);
+    }
+
+    return std::make_pair(std::move(task), std::move(fu));
+  }
+  else {
+    static_assert(dependent_false_v<F>, "invalid async callable");
+  }
 }
 
 // ----------------------------------------------------------------------------
@@ -267,18 +247,18 @@ inline void Executor::_process_async_dependent(
 
   add_successor:
 
-  auto target = Node::AsyncState::UNFINISHED;
+  auto target = ASTATE::UNFINISHED;
   
   // acquires the lock
-  if(state.compare_exchange_weak(target, Node::AsyncState::LOCKED,
+  if(state.compare_exchange_weak(target, ASTATE::LOCKED,
                                  std::memory_order_acq_rel,
                                  std::memory_order_acquire)) {
     task._node->_successors.push_back(node);
-    state.store(Node::AsyncState::UNFINISHED, std::memory_order_release);
+    state.store(ASTATE::UNFINISHED, std::memory_order_release);
   }
   // dep's state is FINISHED, which means dep finished its callable already
   // thus decrement the node's join counter by 1
-  else if (target == Node::AsyncState::FINISHED) {
+  else if (target == ASTATE::FINISHED) {
     num_dependents = node->_join_counter.fetch_sub(1, std::memory_order_acq_rel) - 1;
   }
   // another worker adding its async task to the same successors of this node
@@ -287,37 +267,32 @@ inline void Executor::_process_async_dependent(
   }
 }
 
-
 // Procedure: _tear_down_dependent_async
-inline void Executor::_tear_down_dependent_async(Worker& worker, Node* node) {
+inline void Executor::_tear_down_dependent_async(Worker& worker, Node* node, Node*& cache) {
 
   auto handle = std::get_if<Node::DependentAsync>(&(node->_handle));
 
   // this async task comes from Executor
-  auto target = Node::AsyncState::UNFINISHED;
+  auto target = ASTATE::UNFINISHED;
 
-  while(!handle->state.compare_exchange_weak(target, Node::AsyncState::FINISHED,
+  while(!handle->state.compare_exchange_weak(target, ASTATE::FINISHED,
                                              std::memory_order_acq_rel,
                                              std::memory_order_relaxed)) {
-    target = Node::AsyncState::UNFINISHED;
+    target = ASTATE::UNFINISHED;
   }
   
-  // spaw successors whenever their dependencies are resolved
-  worker._cache = nullptr;
+  // spawn successors whenever their dependencies are resolved
   for(size_t i=0; i<node->_successors.size(); ++i) {
     if(auto s = node->_successors[i]; 
       s->_join_counter.fetch_sub(1, std::memory_order_acq_rel) == 1
     ) {
-      if(worker._cache) {
-        _schedule(worker, worker._cache);
-      }
-      worker._cache = s;
+      _update_cache(worker, cache, s);
     }
   }
   
   // now the executor no longer needs to retain ownership
   if(handle->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
-    node_pool.recycle(node);
+    recycle(node);
   }
 
   _decrement_topology();
similarity index 85%
rename from bundled/taskflow-3.7.0/taskflow/core/async_task.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/async_task.hpp
index 026e8cb1c71352f94f9134518f6de33d91727c74..81f52e1915cb1b71abedbd5480ad82b4fb20c283 100644 (file)
@@ -14,10 +14,10 @@ namespace tf {
 // ----------------------------------------------------------------------------
 
 /**
-@brief class to create a dependent asynchronous task
+@brief class to create a dependent asynchronous task (async task)
 
 A tf::AsyncTask is a lightweight handle that retains @em shared ownership
-of a dependent async task created by an executor.
+of a dependent asynchronous task (async task) created by an executor.
 This shared ownership ensures that the async task remains alive when
 adding it to the dependency list of another async task, 
 thus avoiding the classical [ABA problem](https://en.wikipedia.org/wiki/ABA_problem).
@@ -51,22 +51,22 @@ class AsyncTask {
     AsyncTask() = default;
     
     /**
-    @brief destroys the managed asynchronous task if this is the last owner
+    @brief destroys the managed async task if this is the last owner
     */
     ~AsyncTask();
     
     /**
-    @brief constructs an asynchronous task that shares ownership of @c rhs
+    @brief constructs an async task that shares ownership of @c rhs
     */
     AsyncTask(const AsyncTask& rhs);
 
     /**
-    @brief move-constructs an asynchronous task from @c rhs
+    @brief move-constructs an async task from @c rhs
     */
     AsyncTask(AsyncTask&& rhs);
     
     /**
-    @brief copy-assigns the asynchronous task from @c rhs
+    @brief copy-assigns the async task from @c rhs
 
     Releases the managed object of @c this and retains a new shared ownership
     of @c rhs.
@@ -74,14 +74,14 @@ class AsyncTask {
     AsyncTask& operator = (const AsyncTask& rhs);
 
     /**
-    @brief move-assigns the asynchronous task from @c rhs
+    @brief move-assigns the async task from @c rhs
     
     Releases the managed object of @c this and takes over the ownership of @c rhs.
     */
     AsyncTask& operator = (AsyncTask&& rhs);
     
     /**
-    @brief checks if the asynchronous task stores nothing
+    @brief checks if this async task is associated with a callable
     */
     bool empty() const;
 
@@ -91,18 +91,18 @@ class AsyncTask {
     void reset();
     
     /**
-    @brief obtains a hash value of this asynchronous task
+    @brief obtains the hashed value of this async task
     */
     size_t hash_value() const;
 
     /**
     @brief returns the number of shared owners that are currently managing 
-           this asynchronous task
+           this async task
     */
     size_t use_count() const;
 
     /**                                                                                                       
-    @brief returns the boolean indicating whether the async task is done
+    @brief checks if the async task finishes
     */
     bool is_done() const; 
 
@@ -135,7 +135,7 @@ inline void AsyncTask::_decref() {
   if(_node && std::get_if<Node::DependentAsync>(&(_node->_handle))->use_count.fetch_sub(
       1, std::memory_order_acq_rel
     ) == 1) {
-    node_pool.recycle(_node);
+    recycle(_node);
   }
 }
 
@@ -200,7 +200,7 @@ inline size_t AsyncTask::use_count() const {
 inline bool AsyncTask::is_done() const {
   return std::get_if<Node::DependentAsync>(&(_node->_handle))->state.load(
     std::memory_order_acquire
-  ) == Node::AsyncState::FINISHED;
+  ) == ASTATE::FINISHED;
 }
 
 }  // end of namespace tf ----------------------------------------------------
diff --git a/bundled/taskflow-3.10.0/taskflow/core/atomic_notifier.hpp b/bundled/taskflow-3.10.0/taskflow/core/atomic_notifier.hpp
new file mode 100644 (file)
index 0000000..851a11c
--- /dev/null
@@ -0,0 +1,226 @@
+#if __cplusplus >= TF_CPP20
+
+#pragma once
+
+#include <atomic>
+#include <thread>
+#include <vector>
+
+namespace tf {
+
+class AtomicNotifier {
+
+  friend class Executor;
+
+  public:
+  
+  struct Waiter {
+    alignas (2*TF_CACHELINE_SIZE) uint32_t epoch;
+  };
+
+  AtomicNotifier(size_t N) noexcept : _state(0), _waiters(N) {}
+  ~AtomicNotifier() { assert((_state.load() & WAITER_MASK) == 0); } 
+
+  void notify_one() noexcept;
+  void notify_all() noexcept;
+  void notify_n(size_t n) noexcept;
+  void prepare_wait(Waiter*) noexcept;
+  void cancel_wait(Waiter*) noexcept;
+  void commit_wait(Waiter*) noexcept;
+  size_t size() const noexcept;
+
+ private:
+
+  AtomicNotifier(const AtomicNotifier&) = delete;
+  AtomicNotifier(AtomicNotifier&&) = delete;
+  AtomicNotifier& operator=(const AtomicNotifier&) = delete;
+  AtomicNotifier& operator=(AtomicNotifier&&) = delete;
+
+  // This requires 64-bit
+  static_assert(sizeof(int) == 4, "bad platform");
+  static_assert(sizeof(uint32_t) == 4, "bad platform");
+  static_assert(sizeof(uint64_t) == 8, "bad platform");
+  static_assert(sizeof(std::atomic<uint64_t>) == 8, "bad platform");
+
+  // _state stores the epoch in the most significant 32 bits and the
+  // waiter count in the least significant 32 bits.
+  std::atomic<uint64_t> _state;
+  std::vector<Waiter> _waiters;
+
+  static constexpr uint64_t WAITER_INC  {1};
+  static constexpr size_t   EPOCH_SHIFT {32};
+  static constexpr uint64_t EPOCH_INC   {uint64_t(1) << EPOCH_SHIFT};
+  static constexpr uint64_t WAITER_MASK {EPOCH_INC - 1};
+};
+
+inline void AtomicNotifier::notify_one() noexcept {
+  uint64_t prev = _state.fetch_add(EPOCH_INC, std::memory_order_acq_rel);
+  if(TF_UNLIKELY(prev & WAITER_MASK))  { // has waiter (typically unlikely)
+    _state.notify_one();
+  }
+}
+
+inline void AtomicNotifier::notify_all() noexcept {
+  uint64_t prev = _state.fetch_add(EPOCH_INC, std::memory_order_acq_rel);
+  if(TF_UNLIKELY(prev & WAITER_MASK))  { // has waiter (typically unlikely)
+    _state.notify_all();
+  }
+}
+  
+inline void AtomicNotifier::notify_n(size_t n) noexcept {
+  if(n >= _waiters.size()) {
+    notify_all();
+  }
+  else {
+    for(size_t k=0; k<n; ++k) {
+      notify_one();
+    }
+  }
+}
+
+inline size_t AtomicNotifier::size() const noexcept {
+  return _waiters.size();
+}
+
+inline void AtomicNotifier::prepare_wait(Waiter* waiter) noexcept {
+  uint64_t prev = _state.fetch_add(WAITER_INC, std::memory_order_acq_rel);
+  waiter->epoch = (prev >> EPOCH_SHIFT);
+}
+
+inline void AtomicNotifier::cancel_wait(Waiter*) noexcept {
+  // memory_order_relaxed would suffice for correctness, but the faster
+  // #waiters gets to 0, the less likely it is that we'll do spurious wakeups
+  // (and thus system calls).
+  _state.fetch_sub(WAITER_INC, std::memory_order_seq_cst);
+}
+
+inline void AtomicNotifier::commit_wait(Waiter* waiter) noexcept {
+  uint64_t prev = _state.load(std::memory_order_acquire);
+  while((prev >> EPOCH_SHIFT) == waiter->epoch) {
+    _state.wait(prev, std::memory_order_acquire); 
+    prev = _state.load(std::memory_order_acquire);
+  }
+  // memory_order_relaxed would suffice for correctness, but the faster
+  // #waiters gets to 0, the less likely it is that we'll do spurious wakeups
+  // (and thus system calls)
+  _state.fetch_sub(WAITER_INC, std::memory_order_seq_cst);
+}
+
+//-----------------------------------------------------------------------------
+
+class AtomicNotifierV2 {
+
+  friend class Executor;
+
+  public:
+  
+  struct Waiter {
+    alignas (2*TF_CACHELINE_SIZE) uint32_t epoch;
+  };
+
+  AtomicNotifierV2(size_t N) noexcept : _state(0), _waiters(N) {}
+  ~AtomicNotifierV2() { assert((_state.load() & WAITER_MASK) == 0); } 
+
+  void notify_one() noexcept;
+  void notify_all() noexcept;
+  void notify_n(size_t n) noexcept;
+  void prepare_wait(Waiter*) noexcept;
+  void cancel_wait(Waiter*) noexcept;
+  void commit_wait(Waiter*) noexcept;
+  size_t size() const noexcept;
+
+ private:
+
+  AtomicNotifierV2(const AtomicNotifierV2&) = delete;
+  AtomicNotifierV2(AtomicNotifierV2&&) = delete;
+  AtomicNotifierV2& operator=(const AtomicNotifierV2&) = delete;
+  AtomicNotifierV2& operator=(AtomicNotifierV2&&) = delete;
+
+  // This requires 64-bit
+  static_assert(sizeof(int) == 4, "bad platform");
+  static_assert(sizeof(uint32_t) == 4, "bad platform");
+  static_assert(sizeof(uint64_t) == 8, "bad platform");
+  static_assert(sizeof(std::atomic<uint64_t>) == 8, "bad platform");
+
+  // _state stores the epoch in the most significant 32 bits and the
+  // waiter count in the least significant 32 bits.
+  std::atomic<uint64_t> _state;
+  std::vector<Waiter> _waiters;
+
+  static constexpr uint64_t WAITER_INC  {1};
+  static constexpr uint64_t EPOCH_SHIFT {32};
+  static constexpr uint64_t EPOCH_INC   {uint64_t(1) << EPOCH_SHIFT};
+  static constexpr uint64_t WAITER_MASK {EPOCH_INC - 1};
+};
+
+inline void AtomicNotifierV2::notify_one() noexcept {
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+  //if((_state.load(std::memory_order_acquire) & WAITER_MASK) != 0) {
+  //  _state.fetch_add(EPOCH_INC, std::memory_order_release);
+  //  _state.notify_one(); 
+  //}
+
+  for(uint64_t state = _state.load(std::memory_order_acquire); state & WAITER_MASK;) {
+    if(_state.compare_exchange_weak(state, state + EPOCH_INC, std::memory_order_acquire)) {
+      _state.notify_one(); 
+      break;
+    }
+  }
+}
+
+inline void AtomicNotifierV2::notify_all() noexcept {
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+  //if((_state.load(std::memory_order_acquire) & WAITER_MASK) != 0) {
+  //  _state.fetch_add(EPOCH_INC, std::memory_order_release);
+  //  _state.notify_all(); 
+  //}
+  for(uint64_t state = _state.load(std::memory_order_acquire); state & WAITER_MASK;) {
+    if(_state.compare_exchange_weak(state, state + EPOCH_INC, std::memory_order_acquire)) {
+      _state.notify_all(); 
+      break;
+    }
+  }
+}
+  
+inline void AtomicNotifierV2::notify_n(size_t n) noexcept {
+  if(n >= _waiters.size()) {
+    notify_all();
+  }
+  else {
+    for(size_t k=0; k<n; ++k) {
+      notify_one();
+    }
+  }
+}
+
+inline size_t AtomicNotifierV2::size() const noexcept {
+  return _waiters.size();
+}
+
+inline void AtomicNotifierV2::prepare_wait(Waiter* waiter) noexcept {
+  auto prev = _state.fetch_add(WAITER_INC, std::memory_order_relaxed);
+  waiter->epoch = (prev >> EPOCH_SHIFT);
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+}
+
+inline void AtomicNotifierV2::cancel_wait(Waiter*) noexcept {
+  _state.fetch_sub(WAITER_INC, std::memory_order_relaxed);
+}
+
+inline void AtomicNotifierV2::commit_wait(Waiter* waiter) noexcept {
+  uint64_t prev = _state.load(std::memory_order_seq_cst);
+  while((prev >> EPOCH_SHIFT) == waiter->epoch) {
+    _state.wait(prev, std::memory_order_seq_cst); 
+    prev = _state.load(std::memory_order_seq_cst);
+  }
+  // memory_order_relaxed would suffice for correctness, but the faster
+  // #waiters gets to 0, the less likely it is that we'll do spurious wakeups
+  // (and thus system calls)
+  _state.fetch_sub(WAITER_INC, std::memory_order_seq_cst);
+}
+
+
+
+} // namespace taskflow -------------------------------------------------------
+
+#endif
similarity index 98%
rename from bundled/taskflow-3.7.0/taskflow/core/declarations.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/declarations.hpp
index 7763fab0ba10f2c6a3ae47a968ff8f4e290dc614..9a369468554dbeaeadbff8ae9b2f194d8ee57ec9 100644 (file)
@@ -5,6 +5,8 @@ namespace tf {
 // ----------------------------------------------------------------------------
 // taskflow
 // ----------------------------------------------------------------------------
+
+class Algorithm;
 class AsyncTopology;
 class Node;
 class Graph;
diff --git a/bundled/taskflow-3.10.0/taskflow/core/error.hpp b/bundled/taskflow-3.10.0/taskflow/core/error.hpp
new file mode 100644 (file)
index 0000000..18a9f45
--- /dev/null
@@ -0,0 +1,72 @@
+#pragma once
+
+#include <iostream>
+#include <sstream>
+#include <exception>
+
+#include "../utility/stream.hpp"
+
+namespace tf {
+
+// node-specific states
+struct NSTATE {
+
+  using underlying_type = int;
+
+  constexpr static underlying_type NONE        = 0x00000000;  
+  constexpr static underlying_type CONDITIONED = 0x10000000;  
+  constexpr static underlying_type DETACHED    = 0x20000000;  
+  constexpr static underlying_type PREEMPTED   = 0x40000000;  
+
+  // mask to isolate state bits - non-state bits store # weak dependents
+  constexpr static underlying_type MASK        = 0xF0000000;
+};
+
+using nstate_t = NSTATE::underlying_type;
+
+// exception-specific states
+struct ESTATE {
+  
+  using underlying_type = int;  
+  
+  constexpr static underlying_type NONE      = 0; 
+  constexpr static underlying_type EXCEPTION = 1;
+  constexpr static underlying_type CANCELLED = 2;
+  constexpr static underlying_type ANCHORED  = 4;  
+};
+
+using estate_t = ESTATE::underlying_type;
+
+// async-specific states
+struct ASTATE {
+  
+  using underlying_type = int;
+
+  constexpr static underlying_type UNFINISHED = 0;
+  constexpr static underlying_type LOCKED     = 1;
+  constexpr static underlying_type FINISHED   = 2;
+};
+
+using astate_t = ASTATE::underlying_type;
+
+// Procedure: throw_re
+// Throws runtime error under a given error code.
+template <typename... ArgsT>
+//void throw_se(const char* fname, const size_t line, Error::Code c, ArgsT&&... args) {
+void throw_re(const char* fname, const size_t line, ArgsT&&... args) {
+  std::ostringstream oss;
+  oss << "[" << fname << ":" << line << "] ";
+  //ostreamize(oss, std::forward<ArgsT>(args)...);
+  (oss << ... << args);
+#ifdef TF_DISABLE_EXCEPTION_HANDLING
+  std::cerr << oss.str();
+  std::terminate();
+#else
+  throw std::runtime_error(oss.str());
+#endif
+}
+
+}  // ------------------------------------------------------------------------
+
+#define TF_THROW(...) tf::throw_re(__FILE__, __LINE__, __VA_ARGS__);
+
similarity index 71%
rename from bundled/taskflow-3.7.0/taskflow/core/executor.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/executor.hpp
index a9d9dc457dfa8e1f7cf6534c5e139ce749f897fb..2403eb86a89991f799f85ecd2ea02ecbaade66da 100644 (file)
@@ -3,6 +3,7 @@
 #include "observer.hpp"
 #include "taskflow.hpp"
 #include "async_task.hpp"
+#include "freelist.hpp"
 
 /**
 @file executor.hpp
@@ -15,9 +16,10 @@ namespace tf {
 // Executor Definition
 // ----------------------------------------------------------------------------
 
-/** @class Executor
+/** 
+@class Executor
 
-@brief class to create an executor for running a taskflow graph
+@brief class to create an executor 
 
 An executor manages a set of worker threads to run one or multiple taskflows
 using an efficient work-stealing scheduling algorithm.
@@ -53,21 +55,29 @@ class Executor {
   friend class FlowBuilder;
   friend class Subflow;
   friend class Runtime;
+  friend class Algorithm;
 
   public:
 
   /**
   @brief constructs the executor with @c N worker threads
 
-  @param N the number of workers (default std::thread::hardware_concurrency)
-  
+  @param N number of workers (default std::thread::hardware_concurrency)
+  @param wix interface class instance to configure workers' behaviors
+
   The constructor spawns @c N worker threads to run tasks in a
   work-stealing loop. The number of workers must be greater than zero
   or an exception will be thrown.
   By default, the number of worker threads is equal to the maximum
   hardware concurrency returned by std::thread::hardware_concurrency.
+
+  Users can alter the worker behavior, such as changing thread affinity,
+  via deriving an instance from tf::WorkerInterface.
   */
-  explicit Executor(size_t N = std::thread::hardware_concurrency());
+  explicit Executor(
+    size_t N = std::thread::hardware_concurrency(),
+    std::shared_ptr<WorkerInterface> wix = nullptr
+  );
 
   /**
   @brief destructs the executor
@@ -421,7 +431,7 @@ class Executor {
   Unlike the typical flow of calling `tf::Executor::run` series 
   plus waiting on the result, this method must be called by an internal
   worker of this executor. The caller worker will participate in
-  the work-stealing loop of the scheduler, therby avoiding potential
+  the work-stealing loop of the scheduler, thereby avoiding potential
   deadlock caused by blocked waiting.
   
   @code{.cpp}
@@ -533,7 +543,7 @@ class Executor {
   size_t num_taskflows() const;
   
   /**
-  @brief queries the id of the caller thread in this executor
+  @brief queries the id of the caller thread within this executor
 
   Each worker has an unique id in the range of @c 0 to @c N-1 associated with
   its parent executor.
@@ -1035,36 +1045,31 @@ class Executor {
     
   const size_t _MAX_STEALS;
   
-  std::mutex _wsq_mutex;
   std::mutex _taskflows_mutex;
+  
+  std::vector<Worker> _workers;
+  DefaultNotifier _notifier;
 
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
+  std::latch _latch;
   std::atomic<size_t> _num_topologies {0};
-  std::atomic_flag _all_spawned = ATOMIC_FLAG_INIT;
+  std::atomic_flag _done = ATOMIC_FLAG_INIT; 
 #else
+  Latch _latch;
   std::condition_variable _topology_cv;
   std::mutex _topology_mutex;
   size_t _num_topologies {0};
+  std::atomic<bool> _done {0};
 #endif
+
   
-  std::unordered_map<std::thread::id, size_t> _wids;
-  std::vector<std::thread> _threads;
-  std::vector<Worker> _workers;
   std::list<Taskflow> _taskflows;
 
-  Notifier _notifier;
-
-  TaskQueue<Node*> _wsq;
-
-  std::atomic<bool> _done {0};
+  Freelist<Node*> _freelist;
 
+  std::shared_ptr<WorkerInterface> _worker_interface;
   std::unordered_set<std::shared_ptr<ObserverInterface>> _observers;
 
-  Worker* _this_worker();
-  
-  bool _wait_for_task(Worker&, Node*&);
-  bool _invoke_module_task_internal(Worker&, Node*);
-
   void _observer_prologue(Worker&, Node*);
   void _observer_epilogue(Worker&, Node*);
   void _spawn(size_t);
@@ -1072,40 +1077,68 @@ class Executor {
   void _explore_task(Worker&, Node*&);
   void _schedule(Worker&, Node*);
   void _schedule(Node*);
-  void _schedule(Worker&, const SmallVector<Node*>&);
-  void _schedule(const SmallVector<Node*>&);
   void _set_up_topology(Worker*, Topology*);
-  void _set_up_graph(Graph&, Node*, Topology*, int, SmallVector<Node*>&);
   void _tear_down_topology(Worker&, Topology*);
-  void _tear_down_async(Node*);
-  void _tear_down_dependent_async(Worker&, Node*);
-  void _tear_down_invoke(Worker&, Node*);
+  void _tear_down_async(Worker&, Node*, Node*&);
+  void _tear_down_dependent_async(Worker&, Node*, Node*&);
+  void _tear_down_invoke(Worker&, Node*, Node*&);
   void _increment_topology();
   void _decrement_topology();
   void _invoke(Worker&, Node*);
   void _invoke_static_task(Worker&, Node*);
-  void _invoke_subflow_task(Worker&, Node*);
-  void _detach_subflow_task(Worker&, Node*, Graph&);
   void _invoke_condition_task(Worker&, Node*, SmallVector<int>&);
   void _invoke_multi_condition_task(Worker&, Node*, SmallVector<int>&);
-  void _invoke_module_task(Worker&, Node*);
-  void _invoke_async_task(Worker&, Node*);
-  void _invoke_dependent_async_task(Worker&, Node*);
   void _process_async_dependent(Node*, tf::AsyncTask&, size_t&);
   void _process_exception(Worker&, Node*);
   void _schedule_async_task(Node*);
-  void _corun_graph(Worker&, Node*, Graph&);
+  void _update_cache(Worker&, Node*&, Node*);
+
+  bool _wait_for_task(Worker&, Node*&);
+  bool _invoke_subflow_task(Worker&, Node*);
+  bool _invoke_module_task(Worker&, Node*);
+  bool _invoke_module_task_impl(Worker&, Node*, Graph&);
+  bool _invoke_async_task(Worker&, Node*);
+  bool _invoke_dependent_async_task(Worker&, Node*);
+  bool _invoke_runtime_task(Worker&, Node*);
+  bool _invoke_runtime_task_impl(Worker&, Node*, std::function<void(Runtime&)>&);
+  bool _invoke_runtime_task_impl(Worker&, Node*, std::function<void(Runtime&, bool)>&);
+
+  template <typename I>
+  I _set_up_graph(I, I, Topology*, Node*, nstate_t);
   
   template <typename P>
   void _corun_until(Worker&, P&&);
+  
+  template <typename I>
+  void _corun_graph(Worker&, Node*, I, I);
+  
+  template <typename I>
+  void _schedule(Worker&, I, I);
+
+  template <typename I>
+  void _schedule(I, I);
+
+  template <typename I>
+  void _schedule_graph_with_parent(Worker&, I, I, Node*, nstate_t);
+
+  template <typename P, typename F>
+  auto _async(P&&, F&&, Topology*, Node*);
+
+  template <typename P, typename F>
+  void _silent_async(P&&, F&&, Topology*, Node*);
+
 };
 
+#ifndef DOXYGEN_GENERATING_OUTPUT
+
 // Constructor
-inline Executor::Executor(size_t N) :
-  _MAX_STEALS {((N+1) << 1)},
-  _threads    {N},
-  _workers    {N},
-  _notifier   {N} {
+inline Executor::Executor(size_t N, std::shared_ptr<WorkerInterface> wix) :
+  _MAX_STEALS      ((N+1) << 1),
+  _workers         (N),
+  _notifier        (N),
+  _latch           (N+1),
+  _freelist        (N),
+  _worker_interface(std::move(wix)) {
 
   if(N == 0) {
     TF_THROW("executor must define at least one worker");
@@ -1126,12 +1159,16 @@ inline Executor::~Executor() {
   wait_for_all();
 
   // shut down the scheduler
-  _done = true;
 
-  _notifier.notify(true);
+#if __cplusplus >= TF_CPP20
+  _done.test_and_set(std::memory_order_relaxed);
+#else
+  _done = true;
+#endif
+  _notifier.notify_all();
 
-  for(auto& t : _threads){
-    t.join();
+  for(auto& w : _workers) {
+    w._thread.join();
   }
 }
 
@@ -1142,7 +1179,7 @@ inline size_t Executor::num_workers() const noexcept {
 
 // Function: num_topologies
 inline size_t Executor::num_topologies() const {
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
   return _num_topologies.load(std::memory_order_relaxed);
 #else
   return _num_topologies;
@@ -1155,104 +1192,90 @@ inline size_t Executor::num_taskflows() const {
 }
 
 // Function: _this_worker
-inline Worker* Executor::_this_worker() {
-  auto itr = _wids.find(std::this_thread::get_id());
-  return itr == _wids.end() ? nullptr : &_workers[itr->second];
-}
+//inline Worker* Executor::_this_worker() const {
+//  auto w = pt::this_worker;
+//  return (w && w->_executor == this) ? w : nullptr;
+//}
 
 // Function: this_worker_id
 inline int Executor::this_worker_id() const {
-  auto i = _wids.find(std::this_thread::get_id());
-  return i == _wids.end() ? -1 : static_cast<int>(_workers[i->second]._id);
+  auto w = pt::this_worker;
+  return (w && w->_executor == this) ? static_cast<int>(w->_id) : -1;
 }
 
 // Procedure: _spawn
 inline void Executor::_spawn(size_t N) {
 
-#ifdef __cpp_lib_atomic_wait
-#else
-  std::mutex mutex;
-  std::condition_variable cond;
-  size_t n=0;
-#endif
-
+  // Note: we can't declare latch here as a local variable
+  //       since the main thread may leave quicker than other thread
+  //       and then destroy it, causing the other thread to dangle
+  //       with the latch
   for(size_t id=0; id<N; ++id) {
 
     _workers[id]._id = id;
     _workers[id]._vtm = id;
     _workers[id]._executor = this;
     _workers[id]._waiter = &_notifier._waiters[id];
+    _workers[id]._thread = std::thread([&, &w=_workers[id]] () {
 
-    _threads[id] = std::thread([&, &w=_workers[id]] () {
+      pt::this_worker = &w;
 
-#ifdef __cpp_lib_atomic_wait
-      // wait for the caller thread to initialize the ID mapping
-      _all_spawned.wait(false, std::memory_order_acquire);
-      w._thread = &_threads[w._id];
-#else
-      // update the ID mapping of this thread
-      w._thread = &_threads[w._id];
-      {
-        std::scoped_lock lock(mutex);
-        _wids[std::this_thread::get_id()] = w._id;
-        if(n++; n == num_workers()) {
-          cond.notify_one();
-        }
+      // synchronize with the main thread to ensure all worker data
+      // has been set (e.g., _thread)
+      _latch.arrive_and_wait(); 
+      
+      // initialize the random engine and seed for work-stealing
+      w._rdgen.seed(static_cast<std::default_random_engine::result_type>(
+        std::hash<std::thread::id>()(std::this_thread::get_id()))
+      );
+      w._rdvtm = std::uniform_int_distribution<size_t>(0, 2*_workers.size()-2);
+
+      // before entering the work-stealing loop, call the scheduler prologue
+      if(_worker_interface) {
+        _worker_interface->scheduler_prologue(w);
       }
-#endif
 
       Node* t = nullptr;
-      
-      while(1) {
+      std::exception_ptr ptr = nullptr;
+
+      // must use 1 as condition instead of !done because
+      // the previous worker may stop while the following workers
+      // are still preparing for entering the scheduling loop
+      try {
+        while(1) {
 
-        // execute the tasks.
-        _exploit_task(w, t);
+          // execute the tasks.
+          _exploit_task(w, t);
 
-        // wait for tasks
-        if(_wait_for_task(w, t) == false) {
-          break;
+          // wait for tasks
+          if(_wait_for_task(w, t) == false) {
+            break;
+          }
         }
+      } 
+      catch(...) {
+        ptr = std::current_exception();
+      }
+      
+      // call the user-specified epilogue function
+      if(_worker_interface) {
+        _worker_interface->scheduler_epilogue(w, ptr);
       }
 
     });
-    
-    // POSIX-like system can use the following to affine threads to cores 
-    //cpu_set_t cpuset;
-    //CPU_ZERO(&cpuset);
-    //CPU_SET(id, &cpuset);
-    //pthread_setaffinity_np(
-    //  _threads[id].native_handle(), sizeof(cpu_set_t), &cpuset
-    //);
-
-#ifdef __cpp_lib_atomic_wait
-    //_wids[_threads[id].get_id()] = id;
-    _wids.emplace(std::piecewise_construct,
-      std::forward_as_tuple(_threads[id].get_id()), std::forward_as_tuple(id)
-    );
-#endif
-  }
-  
-#ifdef __cpp_lib_atomic_wait
-  _all_spawned.test_and_set(std::memory_order_release);
-  _all_spawned.notify_all();
-#else
-  std::unique_lock<std::mutex> lock(mutex);
-  cond.wait(lock, [&](){ return n==N; });
-#endif
+  } 
+
+  _latch.arrive_and_wait();
 }
 
 // Function: _corun_until
 template <typename P>
 void Executor::_corun_until(Worker& w, P&& stop_predicate) {
   
-  std::uniform_int_distribution<size_t> rdvtm(0, _workers.size()-1);
-
   exploit:
 
   while(!stop_predicate()) {
 
-    //exploit:
-
     if(auto t = w._wsq.pop(); t) {
       _invoke(w, t);
     }
@@ -1261,7 +1284,9 @@ void Executor::_corun_until(Worker& w, P&& stop_predicate) {
 
       explore:
 
-      t = (w._id == w._vtm) ? _wsq.steal() : _workers[w._vtm]._wsq.steal();
+      //t = (w._id == w._vtm) ? _freelist.steal(w._id) : _workers[w._vtm]._wsq.steal();
+      t = (w._vtm < _workers.size()) ? _workers[w._vtm]._wsq.steal() : 
+                                       _freelist.steal(w._vtm - _workers.size());
 
       if(t) {
         _invoke(w, t);
@@ -1271,7 +1296,10 @@ void Executor::_corun_until(Worker& w, P&& stop_predicate) {
         if(num_steals++ > _MAX_STEALS) {
           std::this_thread::yield();
         }
-        w._vtm = rdvtm(w._rdgen);
+        // skip worker-id
+        //auto r = w._rdgen.random_range(0, 2*_workers.size()-2);
+        auto r = w._rdvtm(w._rdgen);
+        w._vtm = r + (r >= w._id);
         goto explore;
       }
       else {
@@ -1284,32 +1312,39 @@ void Executor::_corun_until(Worker& w, P&& stop_predicate) {
 // Function: _explore_task
 inline void Executor::_explore_task(Worker& w, Node*& t) {
 
-  //assert(_workers[w].wsq.empty());
   //assert(!t);
 
   size_t num_steals = 0;
-  size_t num_yields = 0;
 
-  std::uniform_int_distribution<size_t> rdvtm(0, _workers.size()-1);
-  
   // Here, we write do-while to make the worker steal at once
   // from the assigned victim.
   do {
-    t = (w._id == w._vtm) ? _wsq.steal() : _workers[w._vtm]._wsq.steal();
+    //t = (w._id == w._vtm) ? _freelist.steal(w._id) : _workers[w._vtm]._wsq.steal();
+    t = (w._vtm < _workers.size()) ? _workers[w._vtm]._wsq.steal() : 
+                                     _freelist.steal(w._vtm - _workers.size());
 
     if(t) {
       break;
     }
 
-    if(num_steals++ > _MAX_STEALS) {
+    if (++num_steals > _MAX_STEALS) {
       std::this_thread::yield();
-      if(num_yields++ > 100) {
+      if (num_steals > _MAX_STEALS + 100) {
         break;
       }
     }
 
-    w._vtm = rdvtm(w._rdgen);
-  } while(!_done);
+    // skip worker-id
+    //auto r = w._rdgen.random_range(0, 2*_workers.size()-2);
+    auto r = w._rdvtm(w._rdgen);
+    w._vtm = r + (r >= w._id);
+  } 
+#if __cplusplus >= TF_CPP20
+  // the _DONE can be checked later in wait_for_task?
+  while(!_done.test(std::memory_order_relaxed));
+#else
+  while(!_done);
+#endif
 
 }
 
@@ -1327,26 +1362,34 @@ inline bool Executor::_wait_for_task(Worker& worker, Node*& t) {
   explore_task:
 
   _explore_task(worker, t);
-  
-  // The last thief who successfully stole a task will wake up
-  // another thief worker to avoid starvation.
+
   if(t) {
-    _notifier.notify(false);
     return true;
   }
-
+  
+  // The last thief who successfully stole a task will wake up
+  // another thief worker to avoid starvation.
+//  if(t) {
+//    _notifier.notify_one();
+//    return true;
+//  }
+  
   // ---- 2PC guard ----
   _notifier.prepare_wait(worker._waiter);
 
-  if(!_wsq.empty()) {
+  if(!_freelist.empty()) {
     _notifier.cancel_wait(worker._waiter);
     worker._vtm = worker._id;
     goto explore_task;
   }
-  
+
+#if __cplusplus >= TF_CPP20
+  if(_done.test(std::memory_order_relaxed)) {
+#else
   if(_done) {
+#endif
     _notifier.cancel_wait(worker._waiter);
-    _notifier.notify(true);
+    _notifier.notify_all();
     return false;
   }
   
@@ -1360,10 +1403,10 @@ inline bool Executor::_wait_for_task(Worker& worker, Node*& t) {
     }
   }
   
-  // Now I really need to relinguish my self to others
+  // Now I really need to relinquish my self to others
   _notifier.commit_wait(worker._waiter);
-
   goto explore_task;
+
 }
 
 // Function: make_observer
@@ -1405,140 +1448,125 @@ inline size_t Executor::num_observers() const noexcept {
 // Procedure: _schedule
 inline void Executor::_schedule(Worker& worker, Node* node) {
   
-  // We need to fetch p before the release such that the read 
-  // operation is synchronized properly with other thread to
-  // void data race.
-  auto p = node->_priority;
-
-  node->_state.fetch_or(Node::READY, std::memory_order_release);
-
   // caller is a worker to this pool - starting at v3.5 we do not use
   // any complicated notification mechanism as the experimental result
   // has shown no significant advantage.
   if(worker._executor == this) {
-    worker._wsq.push(node, p);
-    _notifier.notify(false);
+    worker._wsq.push(node, [&](){ _freelist.push(worker._id, node); });
+    _notifier.notify_one();
     return;
   }
-
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    _wsq.push(node, p);
-  }
-
-  _notifier.notify(false);
+  
+  _freelist.push(node);
+  _notifier.notify_one();
 }
 
 // Procedure: _schedule
 inline void Executor::_schedule(Node* node) {
-  
-  // We need to fetch p before the release such that the read 
-  // operation is synchronized properly with other thread to
-  // void data race.
-  auto p = node->_priority;
-
-  node->_state.fetch_or(Node::READY, std::memory_order_release);
-
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    _wsq.push(node, p);
-  }
-
-  _notifier.notify(false);
+  _freelist.push(node);
+  _notifier.notify_one();
 }
 
 // Procedure: _schedule
-inline void Executor::_schedule(Worker& worker, const SmallVector<Node*>& nodes) {
-
-  // We need to cacth the node count to avoid accessing the nodes
-  // vector while the parent topology is removed!
-  const auto num_nodes = nodes.size();
+template <typename I>
+void Executor::_schedule(Worker& worker, I first, I last) {
 
+  size_t num_nodes = last - first;
+  
   if(num_nodes == 0) {
     return;
   }
-
-  // caller is a worker to this pool - starting at v3.5 we do not use
-  // any complicated notification mechanism as the experimental result
-  // has shown no significant advantage.
+  
+  // [NOTE]: We cannot use first/last as the for-loop condition 
+  // (e.g., for(; first != last; ++first)) since when a node is inserted
+  // into the queue the node can run and finish immediately.
+  // If this is the last node in the graph, it will tear down the parent
+  // container which cause the last ++first to fail.
+  // This problem is specific to MSVC which has strict iterator arithmetics.
   if(worker._executor == this) {
-    for(size_t i=0; i<num_nodes; ++i) {
-      // We need to fetch p before the release such that the read 
-      // operation is synchronized properly with other thread to
-      // void data race.
-      auto p = nodes[i]->_priority;
-      nodes[i]->_state.fetch_or(Node::READY, std::memory_order_release);
-      worker._wsq.push(nodes[i], p);
-      _notifier.notify(false);
+    for(size_t i=0; i<num_nodes; i++) {
+      auto node = detail::get_node_ptr(first[i]);
+      worker._wsq.push(node, [&](){ _freelist.push(worker._id, node); });
+      _notifier.notify_one();
     }
     return;
   }
 
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    for(size_t k=0; k<num_nodes; ++k) {
-      auto p = nodes[k]->_priority;
-      nodes[k]->_state.fetch_or(Node::READY, std::memory_order_release);
-      _wsq.push(nodes[k], p);
-    }
+  for(size_t i=0; i<num_nodes; i++) {
+    _freelist.push(detail::get_node_ptr(first[i]));
   }
-
   _notifier.notify_n(num_nodes);
 }
 
 // Procedure: _schedule
-inline void Executor::_schedule(const SmallVector<Node*>& nodes) {
-
-  // parent topology may be removed!
-  const auto num_nodes = nodes.size();
+template <typename I>
+inline void Executor::_schedule(I first, I last) {
+  
+  size_t num_nodes = last - first;
 
   if(num_nodes == 0) {
     return;
   }
 
-  // We need to fetch p before the release such that the read 
-  // operation is synchronized properly with other thread to
-  // void data race.
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    for(size_t k=0; k<num_nodes; ++k) {
-      auto p = nodes[k]->_priority;
-      nodes[k]->_state.fetch_or(Node::READY, std::memory_order_release);
-      _wsq.push(nodes[k], p);
-    }
+  for(size_t i=0; i<num_nodes; i++) {
+    _freelist.push(detail::get_node_ptr(first[i]));
   }
-
   _notifier.notify_n(num_nodes);
 }
+  
+template <typename I>
+void Executor::_schedule_graph_with_parent(
+  Worker& worker, I beg, I end, Node* parent, nstate_t nstate
+) {
+  auto send = _set_up_graph(beg, end, parent->_topology, parent, nstate);
+  parent->_join_counter.fetch_add(send - beg, std::memory_order_relaxed);
+  _schedule(worker, beg, send);
+}
 
+inline void Executor::_update_cache(Worker& worker, Node*& cache, Node* node) {
+  if(cache) {
+    _schedule(worker, cache);
+  }
+  cache = node;
+}
+  
 // Procedure: _invoke
 inline void Executor::_invoke(Worker& worker, Node* node) {
 
-  // synchronize all outstanding memory operations caused by reordering
-  while(!(node->_state.load(std::memory_order_acquire) & Node::READY));
+  #define TF_INVOKE_CONTINUATION()  \
+  if (cache) {                      \
+    node = cache;                   \
+    goto begin_invoke;              \
+  }
 
   begin_invoke:
+
+  Node* cache {nullptr};
   
-  SmallVector<int> conds;
+  // if this is the second invoke due to preemption, directly jump to invoke task
+  if(node->_nstate & NSTATE::PREEMPTED) {
+    goto invoke_task;
+  }
 
-  // no need to do other things if the topology is cancelled
+  // if the work has been cancelled, there is no need to continue
   if(node->_is_cancelled()) {
-    _tear_down_invoke(worker, node);
+    _tear_down_invoke(worker, node, cache);
+    TF_INVOKE_CONTINUATION();
     return;
   }
 
   // if acquiring semaphore(s) exists, acquire them first
   if(node->_semaphores && !node->_semaphores->to_acquire.empty()) {
-    SmallVector<Node*> nodes;
-    if(!node->_acquire_all(nodes)) {
-      _schedule(worker, nodes);
+    SmallVector<Node*> waiters;
+    if(!node->_acquire_all(waiters)) {
+      _schedule(worker, waiters.begin(), waiters.end());
       return;
     }
-    node->_state.fetch_or(Node::ACQUIRED, std::memory_order_release);
   }
-
-  // condition task
-  //int cond = -1;
+  
+  invoke_task:
+  
+  SmallVector<int> conds;
 
   // switch is faster than nested if-else due to jump table
   switch(node->_handle.index()) {
@@ -1547,10 +1575,20 @@ inline void Executor::_invoke(Worker& worker, Node* node) {
       _invoke_static_task(worker, node);
     }
     break;
+    
+    // runtime task
+    case Node::RUNTIME:{
+      if(_invoke_runtime_task(worker, node)) {
+        return;
+      }
+    }
+    break;
 
     // subflow task
     case Node::SUBFLOW: {
-      _invoke_subflow_task(worker, node);
+      if(_invoke_subflow_task(worker, node)) {
+        return;
+      }
     }
     break;
 
@@ -1568,26 +1606,30 @@ inline void Executor::_invoke(Worker& worker, Node* node) {
 
     // module task
     case Node::MODULE: {
-      _invoke_module_task(worker, node);
+      if(_invoke_module_task(worker, node)) {
+        return;
+      }
     }
     break;
 
     // async task
     case Node::ASYNC: {
-      _invoke_async_task(worker, node);
-      _tear_down_async(node);
-      return ;
+      if(_invoke_async_task(worker, node)) {
+        return;
+      }
+      _tear_down_async(worker, node, cache);
+      TF_INVOKE_CONTINUATION();
+      return;
     }
     break;
 
     // dependent async task
     case Node::DEPENDENT_ASYNC: {
-      _invoke_dependent_async_task(worker, node);
-      _tear_down_dependent_async(worker, node);
-      if(worker._cache) {
-        node = worker._cache;
-        goto begin_invoke;
+      if(_invoke_dependent_async_task(worker, node)) {
+        return;
       }
+      _tear_down_dependent_async(worker, node, cache);
+      TF_INVOKE_CONTINUATION();
       return;
     }
     break;
@@ -1597,33 +1639,26 @@ inline void Executor::_invoke(Worker& worker, Node* node) {
     break;
   }
 
-  //invoke_successors:
-
   // if releasing semaphores exist, release them
   if(node->_semaphores && !node->_semaphores->to_release.empty()) {
-    _schedule(worker, node->_release_all());
+    SmallVector<Node*> waiters;
+    node->_release_all(waiters);
+    _schedule(worker, waiters.begin(), waiters.end());
   }
-  
-  // Reset the join counter to support the cyclic control flow.
+
+  // Reset the join counter with strong dependencies to support cycles.
   // + We must do this before scheduling the successors to avoid race
   //   condition on _dependents.
   // + We must use fetch_add instead of direct assigning
   //   because the user-space call on "invoke" may explicitly schedule 
   //   this task again (e.g., pipeline) which can access the join_counter.
-  if((node->_state.load(std::memory_order_relaxed) & Node::CONDITIONED)) {
-    node->_join_counter.fetch_add(node->num_strong_dependents(), std::memory_order_relaxed);
-  }
-  else {
-    node->_join_counter.fetch_add(node->num_dependents(), std::memory_order_relaxed);
-  }
+  node->_join_counter.fetch_add(
+    node->num_dependents() - (node->_nstate & ~NSTATE::MASK), std::memory_order_relaxed
+  );
 
   // acquire the parent flow counter
-  auto& j = (node->_parent) ? node->_parent->_join_counter :
-                              node->_topology->_join_counter;
-
-  // Here, we want to cache the latest successor with the highest priority
-  worker._cache = nullptr;
-  auto max_p = static_cast<unsigned>(TaskPriority::MAX);
+  auto& join_counter = (node->_parent) ? node->_parent->_join_counter :
+                       node->_topology->_join_counter;
 
   // Invoke the task based on the corresponding type
   switch(node->_handle.index()) {
@@ -1636,17 +1671,8 @@ inline void Executor::_invoke(Worker& worker, Node* node) {
           auto s = node->_successors[cond];
           // zeroing the join counter for invariant
           s->_join_counter.store(0, std::memory_order_relaxed);
-          j.fetch_add(1, std::memory_order_relaxed);
-          if(s->_priority <= max_p) {
-            if(worker._cache) {
-              _schedule(worker, worker._cache);
-            }
-            worker._cache = s;
-            max_p = s->_priority;
-          }
-          else {
-            _schedule(worker, s);
-          }
+          join_counter.fetch_add(1, std::memory_order_relaxed);
+          _update_cache(worker, cache, s);
         }
       }
     }
@@ -1658,37 +1684,22 @@ inline void Executor::_invoke(Worker& worker, Node* node) {
         //if(auto s = node->_successors[i]; --(s->_join_counter) == 0) {
         if(auto s = node->_successors[i]; 
           s->_join_counter.fetch_sub(1, std::memory_order_acq_rel) == 1) {
-          j.fetch_add(1, std::memory_order_relaxed);
-          if(s->_priority <= max_p) {
-            if(worker._cache) {
-              _schedule(worker, worker._cache);
-            }
-            worker._cache = s;
-            max_p = s->_priority;
-          }
-          else {
-            _schedule(worker, s);
-          }
+          join_counter.fetch_add(1, std::memory_order_relaxed);
+          _update_cache(worker, cache, s);
         }
       }
     }
     break;
   }
-
-  // tear_down the invoke
-  _tear_down_invoke(worker, node);
-
-  // perform tail recursion elimination for the right-most child to reduce
-  // the number of expensive pop/push operations through the task queue
-  if(worker._cache) {
-    node = worker._cache;
-    //node->_state.fetch_or(Node::READY, std::memory_order_release);
-    goto begin_invoke;
-  }
+  
+  // clean up the node after execution
+  _tear_down_invoke(worker, node, cache);
+  TF_INVOKE_CONTINUATION();
 }
 
 // Procedure: _tear_down_invoke
-inline void Executor::_tear_down_invoke(Worker& worker, Node* node) {
+inline void Executor::_tear_down_invoke(Worker& worker, Node* node, Node*& cache) {
+  
   // we must check parent first before subtracting the join counter,
   // or it can introduce data race
   if(auto parent = node->_parent; parent == nullptr) {
@@ -1696,22 +1707,16 @@ inline void Executor::_tear_down_invoke(Worker& worker, Node* node) {
       _tear_down_topology(worker, node->_topology);
     }
   }
-  // Here we asssume the parent is in a busy loop (e.g., corun) waiting for
-  // its join counter to become 0.
-  else {
-    //parent->_join_counter.fetch_sub(1, std::memory_order_acq_rel);
-    parent->_join_counter.fetch_sub(1, std::memory_order_release);
+  else {  
+    // needs to fetch every data before join-counter becomes zero at which
+    // the node may be deleted
+    auto state = parent->_nstate;
+    if(parent->_join_counter.fetch_sub(1, std::memory_order_acq_rel) == 1) {
+      if(state & NSTATE::PREEMPTED) {
+        _update_cache(worker, cache, parent);
+      }
+    }
   }
-  //// module task
-  //else {  
-  //  auto id = parent->_handle.index();
-  //  if(parent->_join_counter.fetch_sub(1, std::memory_order_acq_rel) == 1) {
-  //    if(id == Node::MODULE) {
-  //      return parent;
-  //    }
-  //  }
-  //}
-  //return nullptr;
 }
 
 // Procedure: _observer_prologue
@@ -1731,103 +1736,85 @@ inline void Executor::_observer_epilogue(Worker& worker, Node* node) {
 // Procedure: _process_exception
 inline void Executor::_process_exception(Worker&, Node* node) {
 
-  constexpr static auto flag = Topology::EXCEPTION | Topology::CANCELLED;
-  
-  // if the node has a parent, we store the exception in its parent
-  if(auto parent = node->_parent; parent) { 
-    if ((parent->_state.fetch_or(Node::EXCEPTION, std::memory_order_relaxed) & Node::EXCEPTION) == 0) {
-      parent->_exception_ptr = std::current_exception();
+  constexpr static auto flag = ESTATE::EXCEPTION | ESTATE::CANCELLED;
+
+  // find the anchor and mark the entire path with exception so recursive
+  // or nested tasks can be cancelled properly
+  // since exception can come from asynchronous task (with runtime), the node
+  // itself can be anchored
+  auto anchor = node;
+  while(anchor && (anchor->_estate.load(std::memory_order_relaxed) & ESTATE::ANCHORED) == 0) {
+    anchor->_estate.fetch_or(flag, std::memory_order_relaxed);
+    anchor = anchor->_parent;
+  }
+
+  // the exception occurs under a blocking call (e.g., corun, join)
+  if(anchor) {
+    // multiple tasks may throw, and we only take the first thrown exception
+    if((anchor->_estate.fetch_or(flag, std::memory_order_relaxed) & ESTATE::EXCEPTION) == 0) {
+      anchor->_exception_ptr = std::current_exception();
+      return;
     }
-    // TODO if the node has a topology, cancel it to enable early stop
-    //if(auto tpg = node->_topology; tpg) {
-    //  tpg->_state.fetch_or(Topology::CANCELLED, std::memory_order_relaxed);
-    //}
   }
-  // multiple tasks may throw, so we only take the first thrown exception
-  else if(auto tpg = node->_topology; tpg && 
-    ((tpg->_state.fetch_or(flag, std::memory_order_relaxed) & Topology::EXCEPTION) == 0)
-  ) {
-    tpg->_exception_ptr = std::current_exception();
+  // otherwise, we simply store the exception in the topology and cancel it
+  else if(auto tpg = node->_topology; tpg) {
+    // multiple tasks may throw, and we only take the first thrown exception
+    if((tpg->_estate.fetch_or(flag, std::memory_order_relaxed) & ESTATE::EXCEPTION) == 0) {
+      tpg->_exception_ptr = std::current_exception();
+      return;
+    }
   }
-  // TODO: skip the exception that is not associated with any taskflows
+  
+  // for now, we simply store the exception in this node; this can happen in an 
+  // execution that does not have any external control to capture the exception,
+  // such as silent async task
+  node->_exception_ptr = std::current_exception();
 }
 
 // Procedure: _invoke_static_task
 inline void Executor::_invoke_static_task(Worker& worker, Node* node) {
   _observer_prologue(worker, node);
   TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
-    auto& work = std::get_if<Node::Static>(&node->_handle)->work;
-    switch(work.index()) {
-      case 0:
-        std::get_if<0>(&work)->operator()();
-      break;
-
-      case 1:
-        Runtime rt(*this, worker, node);
-        std::get_if<1>(&work)->operator()(rt);
-        node->_process_exception();
-      break;
-    }
+    std::get_if<Node::Static>(&node->_handle)->work();
   });
   _observer_epilogue(worker, node);
 }
 
 // Procedure: _invoke_subflow_task
-inline void Executor::_invoke_subflow_task(Worker& w, Node* node) {
-  _observer_prologue(w, node);
-  TF_EXECUTOR_EXCEPTION_HANDLER(w, node, {
-    auto handle = std::get_if<Node::Subflow>(&node->_handle);
-    handle->subgraph._clear();
-    Subflow sf(*this, w, node, handle->subgraph);
-    handle->work(sf);
-    if(sf._joinable) {
-      _corun_graph(w, node, handle->subgraph);
-    }
-    node->_process_exception();
-  });
-  _observer_epilogue(w, node);
-}
-
-// Procedure: _detach_subflow_task
-inline void Executor::_detach_subflow_task(Worker& w, Node* p, Graph& g) {
-
-  // graph is empty and has no async tasks
-  if(g.empty() && p->_join_counter.load(std::memory_order_acquire) == 0) {
-    return;
-  }
-
-  SmallVector<Node*> src;
-  _set_up_graph(g, nullptr, p->_topology, Node::DETACHED, src);
+inline bool Executor::_invoke_subflow_task(Worker& worker, Node* node) {
 
-  {
-    std::lock_guard<std::mutex> lock(p->_topology->_taskflow._mutex);
-    p->_topology->_taskflow._graph._merge(std::move(g));
-  }
+  if((node->_nstate & NSTATE::PREEMPTED) == 0) {
+    
+    auto& h = *std::get_if<Node::Subflow>(&node->_handle);
+    auto& g = h.subgraph;
 
-  p->_topology->_join_counter.fetch_add(src.size(), std::memory_order_relaxed);
-  _schedule(w, src);
-}
+    // set up the subflow
+    Subflow sf(*this, worker, node, g);
 
-// Procedure: _corun_graph
-inline void Executor::_corun_graph(Worker& w, Node* p, Graph& g) {
+    // invoke the subflow callable
+    _observer_prologue(worker, node);
+    TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
+      h.work(sf);
+    });
+    _observer_epilogue(worker, node);
+    
+    // spawn the subflow if it is joinable and its graph is non-empty
+    // implicit join is faster than Subflow::join as it does not involve corun
+    if(sf.joinable() && g.size() > sf._tag) {
 
-  // assert(p);
+      // signal the executor to preempt this node
+      node->_nstate |= NSTATE::PREEMPTED;
 
-  // graph is empty and has no async tasks (subflow)
-  if(g.empty() && p->_join_counter.load(std::memory_order_acquire) == 0) {
-    return;
+      // set up and schedule the graph
+      _schedule_graph_with_parent(worker, g.begin() + sf._tag, g.end(), node, NSTATE::NONE);
+      return true;
+    }
+  }
+  else {
+    node->_nstate &= ~NSTATE::PREEMPTED;
   }
 
-  SmallVector<Node*> src;
-
-  _set_up_graph(g, p, p->_topology, 0, src);
-  p->_join_counter.fetch_add(src.size(), std::memory_order_relaxed);
-  
-  _schedule(w, src);
-
-  _corun_until(w, [p] () -> bool { 
-    return p->_join_counter.load(std::memory_order_acquire) == 0; }
-  );
+  return false;
 }
 
 // Procedure: _invoke_condition_task
@@ -1837,17 +1824,7 @@ inline void Executor::_invoke_condition_task(
   _observer_prologue(worker, node);
   TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
     auto& work = std::get_if<Node::Condition>(&node->_handle)->work;
-    switch(work.index()) {
-      case 0:
-        conds = { std::get_if<0>(&work)->operator()() };
-      break;
-
-      case 1:
-        Runtime rt(*this, worker, node);
-        conds = { std::get_if<1>(&work)->operator()(rt) };
-        node->_process_exception();
-      break;
-    }
+    conds = { work() };
   });
   _observer_epilogue(worker, node);
 }
@@ -1858,87 +1835,98 @@ inline void Executor::_invoke_multi_condition_task(
 ) {
   _observer_prologue(worker, node);
   TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
-    auto& work = std::get_if<Node::MultiCondition>(&node->_handle)->work;
-    switch(work.index()) {
-      case 0:
-        conds = std::get_if<0>(&work)->operator()();
-      break;
-
-      case 1:
-        Runtime rt(*this, worker, node);
-        conds = std::get_if<1>(&work)->operator()(rt);
-        node->_process_exception();
-      break;
-    }
+    conds = std::get_if<Node::MultiCondition>(&node->_handle)->work();
   });
   _observer_epilogue(worker, node);
 }
 
 // Procedure: _invoke_module_task
-inline void Executor::_invoke_module_task(Worker& w, Node* node) {
-  _observer_prologue(w, node);
-  TF_EXECUTOR_EXCEPTION_HANDLER(w, node, {
-    _corun_graph(w, node, std::get_if<Node::Module>(&node->_handle)->graph);
-    node->_process_exception();
-  });
-  _observer_epilogue(w, node);
+inline bool Executor::_invoke_module_task(Worker& w, Node* node) {
+  return _invoke_module_task_impl(w, node, std::get_if<Node::Module>(&node->_handle)->graph);  
+}
+
+// Procedure: _invoke_module_task_impl
+inline bool Executor::_invoke_module_task_impl(Worker& w, Node* node, Graph& graph) {
+
+  // No need to do anything for empty graph
+  if(graph.empty()) {
+    return false;
+  }
+
+  // first entry - not spawned yet
+  if((node->_nstate & NSTATE::PREEMPTED) == 0) {
+    // signal the executor to preempt this node
+    node->_nstate |= NSTATE::PREEMPTED;
+    _schedule_graph_with_parent(w, graph.begin(), graph.end(), node, NSTATE::NONE);
+    return true;
+  }
+  // second entry - already spawned
+  else {
+    node->_nstate &= ~NSTATE::PREEMPTED;
+  }
+  return false;
 }
 
-//// Function: _invoke_module_task_internal
-//inline bool Executor::_invoke_module_task_internal(Worker& w, Node* p) {
-//  
-//  // acquire the underlying graph
-//  auto& g = std::get_if<Node::Module>(&p->_handle)->graph;
-//
-//  // no need to do anything if the graph is empty
-//  if(g.empty()) {
-//    return false;
-//  }
-//
-//  SmallVector<Node*> src;
-//  _set_up_graph(g, p, p->_topology, 0, src);
-//  p->_join_counter.fetch_add(src.size(), std::memory_order_relaxed);
-//
-//  _schedule(w, src);
-//  return true;
-//}
 
 // Procedure: _invoke_async_task
-inline void Executor::_invoke_async_task(Worker& worker, Node* node) {
-  _observer_prologue(worker, node);
-  TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
-    auto& work = std::get_if<Node::Async>(&node->_handle)->work;
-    switch(work.index()) {
-      case 0:
+inline bool Executor::_invoke_async_task(Worker& worker, Node* node) {
+  auto& work = std::get_if<Node::Async>(&node->_handle)->work;
+  switch(work.index()) {
+    // void()
+    case 0:
+      _observer_prologue(worker, node);
+      TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
         std::get_if<0>(&work)->operator()();
-      break;
+      });
+      _observer_epilogue(worker, node);
+    break;
+    
+    // void(Runtime&)
+    case 1:
+      if(_invoke_runtime_task_impl(worker, node, *std::get_if<1>(&work))) {
+        return true;
+      }
+    break;
+    
+    // void(Runtime&, bool)
+    case 2:
+      if(_invoke_runtime_task_impl(worker, node, *std::get_if<2>(&work))) {
+        return true;
+      }
+    break;
+  }
 
-      case 1:
-        Runtime rt(*this, worker, node);
-        std::get_if<1>(&work)->operator()(rt);
-      break;
-    }
-  });
-  _observer_epilogue(worker, node);
+  return false;
 }
 
 // Procedure: _invoke_dependent_async_task
-inline void Executor::_invoke_dependent_async_task(Worker& worker, Node* node) {
-  _observer_prologue(worker, node);
-  TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
-    auto& work = std::get_if<Node::DependentAsync>(&node->_handle)->work;
-    switch(work.index()) {
-      case 0:
+inline bool Executor::_invoke_dependent_async_task(Worker& worker, Node* node) {
+  auto& work = std::get_if<Node::DependentAsync>(&node->_handle)->work;
+  switch(work.index()) {
+    // void()
+    case 0:
+      _observer_prologue(worker, node);
+      TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
         std::get_if<0>(&work)->operator()();
-      break;
+      });
+      _observer_epilogue(worker, node);
+    break;
+    
+    // void(Runtime&)
+    case 1:
+      if(_invoke_runtime_task_impl(worker, node, *std::get_if<1>(&work))) {
+        return true;
+      }
+    break;
 
-      case 1:
-        Runtime rt(*this, worker, node);
-        std::get_if<1>(&work)->operator()(rt);
-      break;
-    }
-  });
-  _observer_epilogue(worker, node);
+    // void(Runtime&, bool)
+    case 2:
+      if(_invoke_runtime_task_impl(worker, node, *std::get_if<2>(&work))) {
+        return true;
+      }
+    break;
+  }
+  return false;
 }
 
 // Function: run
@@ -2035,7 +2023,7 @@ tf::Future<void> Executor::run_until(Taskflow& f, P&& p, C&& c) {
     std::lock_guard<std::mutex> lock(f._mutex);
     f._topologies.push(t);
     if(f._topologies.size() == 1) {
-      _set_up_topology(_this_worker(), t.get());
+      _set_up_topology(pt::this_worker, t.get());
     }
   }
 
@@ -2060,36 +2048,53 @@ tf::Future<void> Executor::run_until(Taskflow&& f, P&& pred, C&& c) {
 // Function: corun
 template <typename T>
 void Executor::corun(T& target) {
-  
-  auto w = _this_worker();
 
-  if(w == nullptr) {
+  static_assert(has_graph_v<T>, "target must define a member function 'Graph& graph()'");
+  
+  if(pt::this_worker == nullptr || pt::this_worker->_executor != this) {
     TF_THROW("corun must be called by a worker of the executor");
   }
 
-  Node parent;  // auxiliary parent
-  _corun_graph(*w, &parent, target.graph());
-  parent._process_exception();
+  Node anchor;
+  _corun_graph(*pt::this_worker, &anchor, target.graph().begin(), target.graph().end());
 }
 
 // Function: corun_until
 template <typename P>
 void Executor::corun_until(P&& predicate) {
   
-  auto w = _this_worker();
-
-  if(w == nullptr) {
+  if(pt::this_worker == nullptr || pt::this_worker->_executor != this) {
     TF_THROW("corun_until must be called by a worker of the executor");
   }
 
-  _corun_until(*w, std::forward<P>(predicate));
+  _corun_until(*pt::this_worker, std::forward<P>(predicate));
+}
+
+// Procedure: _corun_graph
+template <typename I>
+void Executor::_corun_graph(Worker& w, Node* p, I first, I last) {
 
-  // TODO: exception?
+  // empty graph
+  if(first == last) {
+    return;
+  }
+  
+  // anchor this parent as the blocking point
+  {
+    AnchorGuard anchor(p);
+    _schedule_graph_with_parent(w, first, last, p, NSTATE::NONE);
+    _corun_until(w, [p] () -> bool { 
+      return p->_join_counter.load(std::memory_order_acquire) == 0; }
+    );
+  }
+
+  // rethrow the exception to the blocker
+  p->_rethrow_exception();
 }
 
 // Procedure: _increment_topology
 inline void Executor::_increment_topology() {
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
   _num_topologies.fetch_add(1, std::memory_order_relaxed);
 #else
   std::lock_guard<std::mutex> lock(_topology_mutex);
@@ -2099,7 +2104,7 @@ inline void Executor::_increment_topology() {
 
 // Procedure: _decrement_topology
 inline void Executor::_decrement_topology() {
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
   if(_num_topologies.fetch_sub(1, std::memory_order_acq_rel) == 1) {
     _num_topologies.notify_all();
   }
@@ -2113,7 +2118,7 @@ inline void Executor::_decrement_topology() {
 
 // Procedure: wait_for_all
 inline void Executor::wait_for_all() {
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
   size_t n = _num_topologies.load(std::memory_order_acquire);
   while(n != 0) {
     _num_topologies.wait(n, std::memory_order_acquire);
@@ -2126,37 +2131,51 @@ inline void Executor::wait_for_all() {
 }
 
 // Function: _set_up_topology
-inline void Executor::_set_up_topology(Worker* worker, Topology* tpg) {
+inline void Executor::_set_up_topology(Worker* w, Topology* tpg) {
 
   // ---- under taskflow lock ----
+  auto& g = tpg->_taskflow._graph;
+  //g._clear_detached();
+  
+  auto send = _set_up_graph(g.begin(), g.end(), tpg, nullptr, NSTATE::NONE);
+  tpg->_join_counter.store(send - g.begin(), std::memory_order_relaxed);
 
-  tpg->_sources.clear();
-  tpg->_taskflow._graph._clear_detached();
-  _set_up_graph(tpg->_taskflow._graph, nullptr, tpg, 0, tpg->_sources);
-  tpg->_join_counter.store(tpg->_sources.size(), std::memory_order_relaxed);
-
-  if(worker) {
-    _schedule(*worker, tpg->_sources);
-  }
-  else {
-    _schedule(tpg->_sources);
-  }
+  w ? _schedule(*w, g.begin(), send) : _schedule(g.begin(), send);
 }
 
 // Function: _set_up_graph
-inline void Executor::_set_up_graph(
-  Graph& g, Node* parent, Topology* tpg, int state, SmallVector<Node*>& src
-) {
-  for(auto node : g._nodes) {
+template <typename I>
+I Executor::_set_up_graph(I first, I last, Topology* tpg, Node* parent, nstate_t state) {
+
+  auto send = first;
+  for(; first != last; ++first) {
+
+    auto node = first->get();
     node->_topology = tpg;
     node->_parent = parent;
-    node->_state.store(state, std::memory_order_relaxed);
-    if(node->num_dependents() == 0) {
-      src.push_back(node);
-    }
+    node->_nstate = state;
+    node->_estate.store(0, std::memory_order_relaxed);
     node->_set_up_join_counter();
     node->_exception_ptr = nullptr;
+
+    // move source to the first partition
+    if(node->num_dependents() == 0) {
+      std::iter_swap(send++, first);
+    }
+
+    // handle-specific clear
+    switch(node->_handle.index()) {
+
+      // clear detached nodes
+      case Node::SUBFLOW: {
+        std::get_if<Node::Subflow>(&node->_handle)->subgraph.clear();
+      } break;
+
+      default:
+      break;
+    }
   }
+  return send;
 }
 
 // Function: _tear_down_topology
@@ -2170,13 +2189,15 @@ inline void Executor::_tear_down_topology(Worker& worker, Topology* tpg) {
   if(!tpg->_exception_ptr && !tpg->cancelled() && !tpg->_pred()) {
     //assert(tpg->_join_counter == 0);
     std::lock_guard<std::mutex> lock(f._mutex);
-    tpg->_join_counter.store(tpg->_sources.size(), std::memory_order_relaxed);
-    _schedule(worker, tpg->_sources);
+    //auto& g = tpg->_taskflow._graph;
+    //tpg->_join_counter.store(tpg->_num_sources, std::memory_order_relaxed);
+    //_schedule(worker, g.begin(), g.begin() + tpg->_num_sources);
+    _set_up_topology(&worker, tpg);
   }
   // case 2: the final run of this topology
   else {
 
-    // TODO: if the topology is cancelled, need to release all semaphores
+    // invoke the callback after each run
     if(tpg->_call != nullptr) {
       tpg->_call();
     }
@@ -2190,7 +2211,7 @@ inline void Executor::_tear_down_topology(Worker& worker, Topology* tpg) {
       f._topologies.pop();
       tpg = f._topologies.front().get();
 
-      // decrement the topology but since this is not the last we don't notify
+      // decrement the topology
       _decrement_topology();
 
       // set up topology needs to be under the lock or it can
@@ -2229,161 +2250,39 @@ inline void Executor::_tear_down_topology(Worker& worker, Topology* tpg) {
 
 inline void Subflow::join() {
 
-  // assert(this_worker().worker == &_worker);
-
-  if(!_joinable) {
-    TF_THROW("subflow not joinable");
+  if(!joinable()) {
+    TF_THROW("subflow already joined or detached");
   }
+    
+  // iterator to the begining of the subflow
+  auto gbeg = _graph.begin() + _tag;
 
-  // only the parent worker can join the subflow
-  _executor._corun_graph(_worker, _parent, _graph);
-
-  // if any exception is caught from subflow tasks, rethrow it
-  _parent->_process_exception();
+  // join here since corun graph may throw exception
+  _tag |= JOINED_BIT;
 
-  _joinable = false;
+  _executor._corun_graph(_worker, _parent, gbeg, _graph.end());
 }
 
 inline void Subflow::detach() {
 
-  // assert(this_worker().worker == &_worker);
-
-  if(!_joinable) {
+  if(!joinable()) {
     TF_THROW("subflow already joined or detached");
   }
 
-  // only the parent worker can detach the subflow
-  _executor._detach_subflow_task(_worker, _parent, _graph);
-  _joinable = false;
-}
-
-// ############################################################################
-// Forward Declaration: Runtime
-// ############################################################################
-
-// Procedure: schedule
-inline void Runtime::schedule(Task task) {
+  if(_graph.size() > _tag) {
+    auto sbeg = _graph.begin() + _tag;
+    auto send = _executor._set_up_graph(
+      sbeg, _graph.end(), _parent->_topology, nullptr, NSTATE::DETACHED
+    );
+    _parent->_topology->_join_counter.fetch_add(send - sbeg, std::memory_order_relaxed);
+    _executor._schedule(_worker, sbeg, send);
+  }
   
-  auto node = task._node;
-  // need to keep the invariant: when scheduling a task, the task must have
-  // zero dependency (join counter is 0)
-  // or we can encounter bug when inserting a nested flow (e.g., module task)
-  node->_join_counter.store(0, std::memory_order_relaxed);
-
-  auto& j = node->_parent ? node->_parent->_join_counter :
-                            node->_topology->_join_counter;
-  j.fetch_add(1, std::memory_order_relaxed);
-  _executor._schedule(_worker, node);
-}
-
-// Procedure: corun
-template <typename T>
-void Runtime::corun(T&& target) {
-  _executor._corun_graph(_worker, _parent, target.graph());
-  _parent->_process_exception();
-}
-
-// Procedure: corun_until
-template <typename P>
-void Runtime::corun_until(P&& predicate) {
-  _executor._corun_until(_worker, std::forward<P>(predicate));
-  // TODO: exception?
-}
-
-// Function: corun_all
-inline void Runtime::corun_all() {
-  _executor._corun_until(_worker, [this] () -> bool { 
-    return _parent->_join_counter.load(std::memory_order_acquire) == 0; 
-  });
-  _parent->_process_exception();
-}
-
-// Destructor
-inline Runtime::~Runtime() {
-  _executor._corun_until(_worker, [this] () -> bool { 
-    return _parent->_join_counter.load(std::memory_order_acquire) == 0; 
-  });
-}
-
-// ------------------------------------
-// Runtime::silent_async series
-// ------------------------------------
-
-// Function: _silent_async
-template <typename P, typename F>
-void Runtime::_silent_async(Worker& w, P&& params, F&& f) {
-
-  _parent->_join_counter.fetch_add(1, std::memory_order_relaxed);
-
-  auto node = node_pool.animate(
-    std::forward<P>(params), _parent->_topology, _parent, 0,
-    std::in_place_type_t<Node::Async>{}, std::forward<F>(f)
-  );
-
-  _executor._schedule(w, node);
-}
-
-// Function: silent_async
-template <typename F>
-void Runtime::silent_async(F&& f) {
-  _silent_async(*_executor._this_worker(), DefaultTaskParams{}, std::forward<F>(f));
+  _tag |= JOINED_BIT;
 }
 
-// Function: silent_async
-template <typename P, typename F>
-void Runtime::silent_async(P&& params, F&& f) {
-  _silent_async(*_executor._this_worker(), std::forward<P>(params), std::forward<F>(f));
-}
-
-// Function: silent_async_unchecked
-template <typename F>
-void Runtime::silent_async_unchecked(F&& f) {
-  _silent_async(_worker, DefaultTaskParams{}, std::forward<F>(f));
-}
-
-// Function: silent_async_unchecked
-template <typename P, typename F>
-void Runtime::silent_async_unchecked(P&& params, F&& f) {
-  _silent_async(_worker, std::forward<P>(params), std::forward<F>(f));
-}
-
-// ------------------------------------
-// Runtime::async series
-// ------------------------------------
-
-// Function: _async
-template <typename P, typename F>
-auto Runtime::_async(Worker& w, P&& params, F&& f) {
-
-  _parent->_join_counter.fetch_add(1, std::memory_order_relaxed);
-
-  using R = std::invoke_result_t<std::decay_t<F>>;
-
-  std::packaged_task<R()> p(std::forward<F>(f));
-  auto fu{p.get_future()};
-
-  auto node = node_pool.animate(
-    std::forward<P>(params), _parent->_topology, _parent, 0, 
-    std::in_place_type_t<Node::Async>{},
-    [p=make_moc(std::move(p))] () mutable { p.object(); }
-  );
-
-  _executor._schedule(w, node);
-
-  return fu;
-}
-
-// Function: async
-template <typename F>
-auto Runtime::async(F&& f) {
-  return _async(*_executor._this_worker(), DefaultTaskParams{}, std::forward<F>(f));
-}
+#endif
 
-// Function: async
-template <typename P, typename F>
-auto Runtime::async(P&& params, F&& f) {
-  return _async(*_executor._this_worker(), std::forward<P>(params), std::forward<F>(f));
-}
 
 
 
similarity index 84%
rename from bundled/taskflow-3.7.0/taskflow/core/flow_builder.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/flow_builder.hpp
index df1d02fc526f52834744d417cd8b1ae0d5eb0a42..75b0bfb84bf4a8937af6d69cd107a02c3d3a0d59 100644 (file)
@@ -51,6 +51,28 @@ class FlowBuilder {
     std::enable_if_t<is_static_task_v<C>, void>* = nullptr
   >
   Task emplace(C&& callable);
+  
+  /**
+  @brief creates a runtime task
+
+  @tparam C callable type constructible from std::function<void(tf::Runtime&)>
+
+  @param callable callable to construct a runtime task
+
+  @return a tf::Task handle
+
+  The following example creates a runtime task.
+
+  @code{.cpp}
+  tf::Task static_task = taskflow.emplace([](tf::Runtime&){});
+  @endcode
+
+  Please refer to @ref RuntimeTasking for details.
+  */
+  template <typename C,
+    std::enable_if_t<is_runtime_task_v<C>, void>* = nullptr
+  >
+  Task emplace(C&& callable);
 
   /**
   @brief creates a dynamic task
@@ -343,7 +365,7 @@ class FlowBuilder {
   }
   @endcode
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   The callable needs to take a single argument of
   the dereferenced iterator type.
 
@@ -353,7 +375,7 @@ class FlowBuilder {
   Task for_each(B first, E last, C callable, P part = P());
   
   /**
-  @brief constructs an STL-styled index-based parallel-for task 
+  @brief constructs an index-based parallel-for task 
 
   @tparam B beginning index type (must be integral)
   @tparam E ending index type (must be integral)
@@ -385,15 +407,51 @@ class FlowBuilder {
   }
   @endcode
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   The callable needs to take a single argument of the integral index type.
 
   Please refer to @ref ParallelIterations for details.
   */
   template <typename B, typename E, typename S, typename C, typename P = DefaultPartitioner>
-  Task for_each_index(
-    B first, E last, S step, C callable, P part = P()
-  );
+  Task for_each_index(B first, E last, S step, C callable, P part = P());
+
+  /**
+  @brief constructs an index range-based parallel-for task
+
+  @tparam R index range type (tf::IndexRange)
+  @tparam C callable type
+  @tparam P partitioner type (default tf::DefaultPartitioner)
+
+  @param range index range 
+  @param callable callable object to apply to each valid index
+  @param part partitioning algorithm to schedule parallel iterations
+
+  @return a tf::Task handle
+
+  The task spawns asynchronous tasks that applies the callable object to 
+  in the range <tt>[first, last)</tt> with the step size.
+
+  @code{.cpp}
+  // [0, 17) with a step size of 2 using tf::IndexRange
+  tf::IndexRange<int> range(0, 17, 2);
+  
+  // parallelize the sequence [0, 2, 4, 6, 8, 10, 12, 14, 16]
+  taskflow.for_each_index(range, [](tf::IndexRange<int> range) {
+    // iterate each index in the subrange
+    for(int i=range.begin(); i<range.end(); i+=range.step_size()) {
+      printf("iterate %d\n", i);
+    }
+  });
+  
+  executor.run(taskflow).wait();
+  @endcode
+
+  The callable needs to take a single argument of type tf::IndexRange.
+
+  Please refer to @ref ParallelIterations for details.
+  */
+  template <typename R, typename C, typename P = DefaultPartitioner>
+  Task for_each_index(R range, C callable, P part = P());
 
   // ------------------------------------------------------------------------
   // transform
@@ -426,7 +484,7 @@ class FlowBuilder {
   }
   @endcode
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   The callable needs to take a single argument of the dereferenced
   iterator type.
   
@@ -467,7 +525,7 @@ class FlowBuilder {
   }
   @endcode
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   The callable needs to take two arguments of dereferenced elements
   from the two input ranges.
   
@@ -484,7 +542,7 @@ class FlowBuilder {
   // ------------------------------------------------------------------------
 
   /**
-  @brief constructs an STL-styled parallel-reduce task
+  @brief constructs an STL-styled parallel-reduction task
 
   @tparam B beginning iterator type
   @tparam E ending iterator type
@@ -511,12 +569,68 @@ class FlowBuilder {
   }
   @endcode
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
 
   Please refer to @ref ParallelReduction for details.
   */
   template <typename B, typename E, typename T, typename O, typename P = DefaultPartitioner>
   Task reduce(B first, E last, T& init, O bop, P part = P());
+
+  /**
+  @brief constructs an index range-based parallel-reduction task
+
+  @tparam R index range type (tf::IndexRange)
+  @tparam T result type
+  @tparam L local reducer type
+  @tparam G global reducer type
+  @tparam P partitioner type (default tf::DefaultPartitioner)
+
+  @param range index range 
+  @param init initial value of the reduction and the storage for the reduced result
+  @param lop binary operator that will be applied locally per worker
+  @param gop binary operator that will be applied globally among worker 
+  @param part partitioning algorithm to schedule parallel iterations
+
+  @return a tf::Task handle
+
+  The task spawns asynchronous tasks to perform parallel reduction over a range with @c init.
+  The reduced result is store in @c init.
+  Unlike the iterator-based reduction, 
+  index range-based reduction is particularly useful for applications that benefit from SIMD optimizations 
+  or other range-based processing strategies.
+
+  @code{.cpp}
+  const size_t N = 1000000;
+  std::vector<int> data(N);  // uninitialized data vector
+  int res = 1;               // res will participate in the reduction
+
+  taskflow.reduce_by_index(
+    tf::IndexRange<size_t>(0, N, 1),
+    // final result
+    res,
+    // local reducer
+    [&](tf::IndexRange<size_t> subrange, std::optional<int> running_total) -> int {
+      int residual = running_total ? *running_total : 0.0;
+      for(size_t i=subrange.begin(); i<subrange.end(); i+=subrange.step_size()) {
+        data[i] = 1.0;
+        residual += data[i];
+      }
+      printf("partial sum = %lf\n", residual);
+      return residual;
+    },
+    // global reducer
+    std::plus<int>()
+  );
+  executor.run(taskflow).wait();
+  assert(res = N + 1);
+  @endcode
+
+  Range can be made stateful by using std::reference_wrapper.
+
+  Please refer to @ref ParallelReduction for details.
+  */
+  template <typename R, typename T, typename L, typename G, typename P = DefaultPartitioner>
+  Task reduce_by_index(R range, T& init, L lop, G gop, P part = P());
   
   // ------------------------------------------------------------------------
   // transform and reduction
@@ -552,7 +666,7 @@ class FlowBuilder {
   }
   @endcode
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
 
   Please refer to @ref ParallelReduction for details.
   */
@@ -593,7 +707,7 @@ class FlowBuilder {
   }
   @endcode
  
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
 
   Please refer to @ref ParallelReduction for details.
   */
@@ -610,28 +724,26 @@ class FlowBuilder {
   // ------------------------------------------------------------------------
   // scan
   // ------------------------------------------------------------------------
-  
-  /**
+
+    /**
   @brief creates an STL-styled parallel inclusive-scan task
 
   @tparam B beginning iterator type
   @tparam E ending iterator type
   @tparam D destination iterator type
   @tparam BOP summation operator type
-  @tparam P partitioner type (default tf::DefaultPartitioner)
 
   @param first start of input range
   @param last end of input range
   @param d_first start of output range (may be the same as input range)
   @param bop function to perform summation
-  @param part partitioning algorithm to schedule parallel iterations
 
   Performs the cumulative sum (aka prefix sum, aka scan) of the input range
-  and writes the result to the output range. 
+  and writes the result to the output range.
   Each element of the output range contains the
   running total of all earlier elements using the given binary operator
   for summation.
-  
+
   This function generates an @em inclusive scan, meaning that the N-th element
   of the output range is the sum of the first N input elements,
   so the N-th input element is included.
@@ -642,18 +754,16 @@ class FlowBuilder {
     input.begin(), input.end(), input.begin(), std::plus<int>{}
   );
   executor.run(taskflow).wait();
-  
+
   // input is {1, 3, 6, 10, 15}
   @endcode
-  
-  Iterators are templated to enable stateful range using std::reference_wrapper.
-  
+
+  Iterators can be made stateful by using std::reference_wrapper
+
   Please refer to @ref ParallelScan for details.
   */
-  template <typename B, typename E, typename D, typename BOP, typename P = DefaultPartitioner,
-    std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>* = nullptr
-  >
-  Task inclusive_scan(B first, E last, D d_first, BOP bop, P part = P());
+  template <typename B, typename E, typename D, typename BOP>
+  Task inclusive_scan(B first, E last, D d_first, BOP bop);
   
   /**
   @brief creates an STL-styled parallel inclusive-scan task with an initial value
@@ -663,14 +773,12 @@ class FlowBuilder {
   @tparam D destination iterator type
   @tparam BOP summation operator type
   @tparam T initial value type
-  @tparam P partitioner type (default tf::DefaultPartitioner)
 
   @param first start of input range
   @param last end of input range
   @param d_first start of output range (may be the same as input range)
   @param bop function to perform summation
   @param init initial value
-  @param part partitioning algorithm to schedule parallel iterations
 
   Performs the cumulative sum (aka prefix sum, aka scan) of the input range
   and writes the result to the output range. 
@@ -692,15 +800,13 @@ class FlowBuilder {
   // input is {0, 2, 5, 9, 14}
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
  
   Please refer to @ref ParallelScan for details.
 
   */
-  template <typename B, typename E, typename D, typename BOP, typename T, typename P = DefaultPartitioner,
-    std::enable_if_t<!is_partitioner_v<std::decay_t<T>>, void>* = nullptr
-  >
-  Task inclusive_scan(B first, E last, D d_first, BOP bop, T init, P part = P());
+  template <typename B, typename E, typename D, typename BOP, typename T>
+  Task inclusive_scan(B first, E last, D d_first, BOP bop, T init);
   
   /**
   @brief creates an STL-styled parallel exclusive-scan task
@@ -710,14 +816,12 @@ class FlowBuilder {
   @tparam D destination iterator type
   @tparam T initial value type
   @tparam BOP summation operator type
-  @tparam P partitioner type (default tf::DefaultPartitioner)
 
   @param first start of input range
   @param last end of input range
   @param d_first start of output range (may be the same as input range)
   @param init initial value
   @param bop function to perform summation
-  @param part partitioning algorithm to schedule parallel iterations
 
   Performs the cumulative sum (aka prefix sum, aka scan) of the input range
   and writes the result to the output range. 
@@ -739,12 +843,12 @@ class FlowBuilder {
   // input is {-1, 0, 2, 5, 9}
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   
   Please refer to @ref ParallelScan for details.
   */
-  template <typename B, typename E, typename D, typename T, typename BOP, typename P = DefaultPartitioner>
-  Task exclusive_scan(B first, E last, D d_first, T init, BOP bop, P part = P());
+  template <typename B, typename E, typename D, typename T, typename BOP>
+  Task exclusive_scan(B first, E last, D d_first, T init, BOP bop);
   
   // ------------------------------------------------------------------------
   // transform scan
@@ -758,14 +862,12 @@ class FlowBuilder {
   @tparam D destination iterator type
   @tparam BOP summation operator type
   @tparam UOP transform operator type
-  @tparam P partitioner type (default tf::DefaultPartitioner)
 
   @param first start of input range
   @param last end of input range
   @param d_first start of output range (may be the same as input range)
   @param bop function to perform summation
   @param uop function to transform elements of the input range
-  @param part partitioning algorithm to schedule parallel iterations
 
   Write the cumulative sum (aka prefix sum, aka scan) of the input range
   to the output range. Each element of the output range contains the
@@ -788,14 +890,12 @@ class FlowBuilder {
   // input is {-1, -3, -6, -10, -15}
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   
   Please refer to @ref ParallelScan for details.
   */
-  template <typename B, typename E, typename D, typename BOP, typename UOP, typename P = DefaultPartitioner,
-    std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>* = nullptr
-  >
-  Task transform_inclusive_scan(B first, E last, D d_first, BOP bop, UOP uop, P part = P());
+  template <typename B, typename E, typename D, typename BOP, typename UOP>
+  Task transform_inclusive_scan(B first, E last, D d_first, BOP bop, UOP uop);
   
   /**
   @brief creates an STL-styled parallel transform-inclusive scan task
@@ -806,7 +906,6 @@ class FlowBuilder {
   @tparam BOP summation operator type
   @tparam UOP transform operator type
   @tparam T initial value type
-  @tparam P partitioner type (default tf::DefaultPartitioner)
 
   @param first start of input range
   @param last end of input range
@@ -814,7 +913,6 @@ class FlowBuilder {
   @param bop function to perform summation
   @param uop function to transform elements of the input range
   @param init initial value
-  @param part partitioning algorithm to schedule parallel iterations
 
   Write the cumulative sum (aka prefix sum, aka scan) of the input range
   to the output range. Each element of the output range contains the
@@ -838,14 +936,12 @@ class FlowBuilder {
   // input is {-2, -4, -7, -11, -16}
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   
   Please refer to @ref ParallelScan for details.
   */
-  template <typename B, typename E, typename D, typename BOP, typename UOP, typename T, typename P = DefaultPartitioner,
-    std::enable_if_t<!is_partitioner_v<std::decay_t<T>>, void>* = nullptr
-  >
-  Task transform_inclusive_scan(B first, E last, D d_first, BOP bop, UOP uop, T init, P part = P());
+  template <typename B, typename E, typename D, typename BOP, typename UOP, typename T>
+  Task transform_inclusive_scan(B first, E last, D d_first, BOP bop, UOP uop, T init);
   
   /**
   @brief creates an STL-styled parallel transform-exclusive scan task
@@ -856,7 +952,6 @@ class FlowBuilder {
   @tparam BOP summation operator type
   @tparam UOP transform operator type
   @tparam T initial value type
-  @tparam P partitioner type (default tf::DefaultPartitioner)
 
   @param first start of input range
   @param last end of input range
@@ -864,7 +959,6 @@ class FlowBuilder {
   @param bop function to perform summation
   @param uop function to transform elements of the input range
   @param init initial value
-  @param part partitioning algorithm to schedule parallel iterations
 
   Write the cumulative sum (aka prefix sum, aka scan) of the input range
   to the output range. Each element of the output range contains the
@@ -887,12 +981,12 @@ class FlowBuilder {
   // input is {-1, -2, -4, -7, -11}
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   
   Please refer to @ref ParallelScan for details.
   */
-  template <typename B, typename E, typename D, typename T, typename BOP, typename UOP, typename P = DefaultPartitioner>
-  Task transform_exclusive_scan(B first, E last, D d_first, T init, BOP bop, UOP uop, P part = P());
+  template <typename B, typename E, typename D, typename T, typename BOP, typename UOP>
+  Task transform_exclusive_scan(B first, E last, D d_first, T init, BOP bop, UOP uop);
 
   // ------------------------------------------------------------------------
   // find
@@ -941,7 +1035,7 @@ class FlowBuilder {
   assert(*result == 22);
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   */
   template <typename B, typename E, typename T, typename UOP, typename P = DefaultPartitioner>
   Task find_if(B first, E last, T &result, UOP predicate, P part = P());
@@ -989,7 +1083,7 @@ class FlowBuilder {
   assert(*result == 22);
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   */
   template <typename B, typename E, typename T, typename UOP, typename P = DefaultPartitioner>
   Task find_if_not(B first, E last, T &result, UOP predicate, P part = P());
@@ -1041,7 +1135,7 @@ class FlowBuilder {
   assert(*result == -1);
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   */
   template <typename B, typename E, typename T, typename C, typename P>
   Task min_element(B first, E last, T& result, C comp, P part);
@@ -1093,7 +1187,7 @@ class FlowBuilder {
   assert(*result == 2);
   @endcode
   
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
   */
   template <typename B, typename E, typename T, typename C, typename P>
   Task max_element(B first, E last, T& result, C comp, P part);
@@ -1116,7 +1210,7 @@ class FlowBuilder {
   The task spawns asynchronous tasks to sort elements in the range
   <tt>[first, last)</tt> in parallel.
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
 
   Please refer to @ref ParallelSort for details.
   */
@@ -1137,7 +1231,7 @@ class FlowBuilder {
   <tt>[first, last)</tt> using the @c std::less<T> comparator,
   where @c T is the dereferenced iterator type.
 
-  Iterators are templated to enable stateful range using std::reference_wrapper.
+  Iterators can be made stateful by using std::reference_wrapper
 
   Please refer to @ref ParallelSort for details.
    */
@@ -1165,15 +1259,23 @@ inline FlowBuilder::FlowBuilder(Graph& graph) :
 // Function: emplace
 template <typename C, std::enable_if_t<is_static_task_v<C>, void>*>
 Task FlowBuilder::emplace(C&& c) {
-  return Task(_graph._emplace_back("", 0, nullptr, nullptr, 0,
+  return Task(_graph._emplace_back("", nullptr, nullptr, 0,
     std::in_place_type_t<Node::Static>{}, std::forward<C>(c)
   ));
 }
 
+// Function: emplace
+template <typename C, std::enable_if_t<is_runtime_task_v<C>, void>*>
+Task FlowBuilder::emplace(C&& c) {
+  return Task(_graph._emplace_back("", nullptr, nullptr, 0,
+    std::in_place_type_t<Node::Runtime>{}, std::forward<C>(c)
+  ));
+}
+
 // Function: emplace
 template <typename C, std::enable_if_t<is_subflow_task_v<C>, void>*>
 Task FlowBuilder::emplace(C&& c) {
-  return Task(_graph._emplace_back("", 0, nullptr, nullptr, 0,
+  return Task(_graph._emplace_back("", nullptr, nullptr, 0,
     std::in_place_type_t<Node::Subflow>{}, std::forward<C>(c)
   ));
 }
@@ -1181,7 +1283,7 @@ Task FlowBuilder::emplace(C&& c) {
 // Function: emplace
 template <typename C, std::enable_if_t<is_condition_task_v<C>, void>*>
 Task FlowBuilder::emplace(C&& c) {
-  return Task(_graph._emplace_back("", 0, nullptr, nullptr, 0,
+  return Task(_graph._emplace_back("", nullptr, nullptr, 0,
     std::in_place_type_t<Node::Condition>{}, std::forward<C>(c)
   ));
 }
@@ -1189,7 +1291,7 @@ Task FlowBuilder::emplace(C&& c) {
 // Function: emplace
 template <typename C, std::enable_if_t<is_multi_condition_task_v<C>, void>*>
 Task FlowBuilder::emplace(C&& c) {
-  return Task(_graph._emplace_back("", 0, nullptr, nullptr, 0,
+  return Task(_graph._emplace_back("", nullptr, nullptr, 0,
     std::in_place_type_t<Node::MultiCondition>{}, std::forward<C>(c)
   ));
 }
@@ -1227,7 +1329,7 @@ inline void FlowBuilder::erase(Task task) {
 // Function: composed_of
 template <typename T>
 Task FlowBuilder::composed_of(T& object) {
-  auto node = _graph._emplace_back("", 0, nullptr, nullptr, 0,
+  auto node = _graph._emplace_back("", nullptr, nullptr, 0,
     std::in_place_type_t<Node::Module>{}, object
   );
   return Task(node);
@@ -1235,7 +1337,7 @@ Task FlowBuilder::composed_of(T& object) {
 
 // Function: placeholder
 inline Task FlowBuilder::placeholder() {
-  auto node = _graph._emplace_back("", 0, nullptr, nullptr, 0,
+  auto node = _graph._emplace_back("", nullptr, nullptr, 0,
     std::in_place_type_t<Node::Placeholder>{}
   );
   return Task(node);
@@ -1276,11 +1378,12 @@ inline void FlowBuilder::linearize(std::initializer_list<Task> keys) {
 
 @brief class to construct a subflow graph from the execution of a dynamic task
 
-tf::Subflow is a derived class from tf::Runtime with a specialized mechanism
-to manage the execution of a child graph.
-By default, a subflow automatically @em joins its parent node.
-You may explicitly join or detach a subflow by calling tf::Subflow::join
+tf::Subflow is spawned from the execution of a task to dynamically manage a 
+child graph that may depend on runtime variables.
+You can explicitly join or detach a subflow by calling tf::Subflow::join
 or tf::Subflow::detach, respectively.
+By default, the %Taskflow runtime will implicitly join a subflow it is is joinable.
+
 The following example creates a taskflow graph that spawns a subflow from
 the execution of task @c B, and the subflow contains three tasks, @c B1,
 @c B2, and @c B3, where @c B3 runs after @c B1 and @c B2.
@@ -1307,15 +1410,13 @@ C.precede(D);  // D runs after C
 @endcode
 
 */
-class Subflow : public FlowBuilder,
-                public Runtime {
+class Subflow : public FlowBuilder {
 
   friend class Executor;
   friend class FlowBuilder;
-  friend class Runtime;
 
   public:
-
+    
     /**
     @brief enables the subflow to join its parent task
 
@@ -1350,17 +1451,6 @@ class Subflow : public FlowBuilder,
     */
     void detach();
 
-    /**
-    @brief resets the subflow to a joinable state
-
-    @param clear_graph specifies whether to clear the associated graph (default @c true)
-
-    Clears the underlying task graph depending on the 
-    given variable @c clear_graph (default @c true) and then
-    updates the subflow to a joinable state.
-    */
-    void reset(bool clear_graph = true);
-
     /**
     @brief queries if the subflow is joinable
 
@@ -1378,33 +1468,63 @@ class Subflow : public FlowBuilder,
     */
     bool joinable() const noexcept;
 
+    /**
+    @brief acquires the associated executor
+    */
+    Executor& executor() noexcept;
+    
+    /**
+    @brief acquires the associated graph
+    */
+    Graph& graph() { return _graph; }
+
   private:
+    
+    // with only the most significant bit set: 1000...000
+    constexpr static size_t JOINED_BIT = (~size_t(0)) ^ ((~size_t(0)) >> 1);
+    
+    Subflow(Executor&, Worker&, Node*, Graph&);
+    
+    Subflow() = delete;
+    Subflow(const Subflow&) = delete;
+    Subflow(Subflow&&) = delete;
 
-    bool _joinable {true};
+    Executor& _executor;
+    Worker& _worker;
+    Node* _parent;
 
-    Subflow(Executor&, Worker&, Node*, Graph&);
+    size_t _tag;
 };
 
 // Constructor
-inline Subflow::Subflow(
-  Executor& executor, Worker& worker, Node* parent, Graph& graph
-) :
-  FlowBuilder {graph},
-  Runtime {executor, worker, parent} {
+inline Subflow::Subflow(Executor& executor, Worker& worker, Node* parent, Graph& graph) :
+  FlowBuilder {graph}, 
+  _executor   {executor}, 
+  _worker     {worker}, 
+  _parent     {parent}, 
+  _tag        {graph.size()} {
+
   // assert(_parent != nullptr);
+  // clear undetached nodes in reversed order
+  for(auto i = graph.rbegin(); i != graph.rend(); ++i) {
+    if((i->get()->_nstate & NSTATE::DETACHED) == 0) {
+      --_tag;
+    }
+    else {
+      break;
+    }
+  }
+  graph.resize(_tag);
 }
 
-// Function: joined
+// Function: joinable
 inline bool Subflow::joinable() const noexcept {
-  return _joinable;
+  return (_tag & JOINED_BIT) == 0;
 }
 
-// Procedure: reset
-inline void Subflow::reset(bool clear_graph) {
-  if(clear_graph) {
-    _graph._clear();
-  }
-  _joinable = true;
+// Function: executor
+inline Executor& Subflow::executor() noexcept {
+  return _executor;
 }
 
 }  // end of namespace tf. ---------------------------------------------------
diff --git a/bundled/taskflow-3.10.0/taskflow/core/freelist.hpp b/bundled/taskflow-3.10.0/taskflow/core/freelist.hpp
new file mode 100644 (file)
index 0000000..658ff94
--- /dev/null
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "tsq.hpp"
+
+namespace tf {
+
+/**
+@private
+*/
+template <typename T>
+class Freelist {
+
+  struct Head {
+    std::mutex mutex;
+    UnboundedTaskQueue<T> queue;
+  };
+
+  public:
+
+  Freelist(size_t N) : _heads(N) {}
+
+  void push(size_t w, T item) {
+    std::scoped_lock lock(_heads[w].mutex);
+    _heads[w].queue.push(item);  
+  }
+
+  void push(T item) {
+    push(reinterpret_cast<uintptr_t>(item) % _heads.size(), item);
+  }
+
+  T steal(size_t w) {
+    return _heads[w].queue.steal();
+  }
+
+
+  bool empty() const {
+    for(const auto& q : _heads) {
+      if(!q.queue.empty()) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private:
+  
+  std::vector<Head> _heads;
+};
+
+
+}  // end of namespace tf -----------------------------------------------------
diff --git a/bundled/taskflow-3.10.0/taskflow/core/graph.hpp b/bundled/taskflow-3.10.0/taskflow/core/graph.hpp
new file mode 100644 (file)
index 0000000..2f6ed5d
--- /dev/null
@@ -0,0 +1,839 @@
+#pragma once
+
+#include "../utility/macros.hpp"
+#include "../utility/traits.hpp"
+#include "../utility/iterator.hpp"
+
+#ifdef TF_ENABLE_TASK_POOL
+#include "../utility/object_pool.hpp"
+#endif
+
+#include "../utility/os.hpp"
+#include "../utility/math.hpp"
+#include "../utility/small_vector.hpp"
+#include "../utility/serializer.hpp"
+#include "../utility/latch.hpp"
+#include "../utility/mpmc.hpp"
+#include "error.hpp"
+#include "declarations.hpp"
+#include "semaphore.hpp"
+#include "environment.hpp"
+#include "topology.hpp"
+#include "tsq.hpp"
+
+/**
+@file graph.hpp
+@brief graph include file
+*/
+
+namespace tf {
+
+// ----------------------------------------------------------------------------
+// Class: Graph
+// ----------------------------------------------------------------------------
+
+
+/**
+@class Graph
+
+@brief class to create a graph object
+
+A graph is the ultimate storage for a task dependency graph and is the main
+gateway to interact with an executor.
+A graph manages a set of nodes in a global object pool that animates and
+recycles node objects efficiently without going through repetitive and
+expensive memory allocations and deallocations.
+This class is mainly used for creating an opaque graph object in a custom
+class to interact with the executor through taskflow composition.
+
+A graph object is move-only.
+*/
+class Graph : public std::vector<std::unique_ptr<Node>> {
+
+  friend class Node;
+  friend class FlowBuilder;
+  friend class Subflow;
+  friend class Taskflow;
+  friend class Executor;
+
+  public:
+
+  /**
+  @brief constructs a graph object
+  */
+  Graph() = default;
+
+  /**
+  @brief disabled copy constructor
+  */
+  Graph(const Graph&) = delete;
+
+  /**
+  @brief constructs a graph using move semantics
+  */
+  Graph(Graph&&) = default;
+
+  /**
+  @brief disabled copy assignment operator
+  */
+  Graph& operator = (const Graph&) = delete;
+
+  /**
+  @brief assigns a graph using move semantics
+  */
+  Graph& operator = (Graph&&) = default;
+  
+
+  private:
+
+  void _erase(Node*);
+  
+  /**
+  @private
+  */
+  template <typename ...ArgsT>
+  Node* _emplace_back(ArgsT&&...);
+};
+
+// ----------------------------------------------------------------------------
+// TaskParams
+// ----------------------------------------------------------------------------
+
+/**
+@struct TaskParams
+
+@brief task parameters to use when creating an asynchronous task
+*/
+struct TaskParams {
+  /**
+  @brief name of the task
+  */
+  std::string name;
+
+  /**
+  @brief C-styled pointer to user data
+  */
+  void* data {nullptr};
+};
+
+/**
+@struct DefaultTaskParams
+
+@brief empty task parameter type for compile-time optimization
+*/
+struct DefaultTaskParams {
+};
+
+/**
+@brief determines if the given type is a task parameter type
+
+Task parameters can be specified in one of the following types:
+  + tf::TaskParams: assign the struct of defined parameters
+  + tf::DefaultTaskParams: assign nothing
+  + std::string: assign a name to the task
+*/
+template <typename P>
+constexpr bool is_task_params_v =
+  std::is_same_v<std::decay_t<P>, TaskParams> ||
+  std::is_same_v<std::decay_t<P>, DefaultTaskParams> ||
+  std::is_constructible_v<std::string, P>;
+
+// ----------------------------------------------------------------------------
+// Node
+// ----------------------------------------------------------------------------
+
+/**
+@private
+*/
+class Node {
+
+  friend class Graph;
+  friend class Task;
+  friend class AsyncTask;
+  friend class TaskView;
+  friend class Taskflow;
+  friend class Executor;
+  friend class FlowBuilder;
+  friend class Subflow;
+  friend class Runtime;
+  friend class AnchorGuard;
+  friend class PreemptionGuard;
+
+  //template <typename T>
+  //friend class Freelist;
+
+#ifdef TF_ENABLE_TASK_POOL
+  TF_ENABLE_POOLABLE_ON_THIS;
+#endif
+
+  using Placeholder = std::monostate;
+
+  // static work handle
+  struct Static {
+
+    template <typename C>
+    Static(C&&);
+
+    std::function<void()> work;
+  };
+  
+  // runtime work handle
+  struct Runtime {
+
+    template <typename C>
+    Runtime(C&&);
+
+    std::function<void(tf::Runtime&)> work;
+  };
+
+  // subflow work handle
+  struct Subflow {
+
+    template <typename C>
+    Subflow(C&&);
+
+    std::function<void(tf::Subflow&)> work;
+    Graph subgraph;
+  };
+
+  // condition work handle
+  struct Condition {
+
+    template <typename C>
+    Condition(C&&);
+    
+    std::function<int()> work;
+  };
+
+  // multi-condition work handle
+  struct MultiCondition {
+
+    template <typename C>
+    MultiCondition(C&&);
+
+    std::function<SmallVector<int>()> work;
+  };
+
+  // module work handle
+  struct Module {
+
+    template <typename T>
+    Module(T&);
+
+    Graph& graph;
+  };
+
+  // Async work
+  struct Async {
+
+    template <typename T>
+    Async(T&&);
+
+    std::variant<
+      std::function<void()>, 
+      std::function<void(tf::Runtime&)>,       // silent async
+      std::function<void(tf::Runtime&, bool)>  // async
+    > work;
+  };
+  
+  // silent dependent async
+  struct DependentAsync {
+    
+    template <typename C>
+    DependentAsync(C&&);
+    
+    std::variant<
+      std::function<void()>, 
+      std::function<void(tf::Runtime&)>,       // silent async
+      std::function<void(tf::Runtime&, bool)>  // async
+    > work;
+   
+    std::atomic<size_t> use_count {1};
+    std::atomic<ASTATE::underlying_type> state {ASTATE::UNFINISHED};
+  };
+
+  using handle_t = std::variant<
+    Placeholder,      // placeholder
+    Static,           // static tasking
+    Runtime,          // runtime tasking
+    Subflow,          // subflow tasking
+    Condition,        // conditional tasking
+    MultiCondition,   // multi-conditional tasking
+    Module,           // composable tasking
+    Async,            // async tasking
+    DependentAsync    // dependent async tasking
+  >;
+
+  struct Semaphores {
+    SmallVector<Semaphore*> to_acquire;
+    SmallVector<Semaphore*> to_release;
+  };
+
+  public:
+
+  // variant index
+  constexpr static auto PLACEHOLDER     = get_index_v<Placeholder, handle_t>;
+  constexpr static auto STATIC          = get_index_v<Static, handle_t>;
+  constexpr static auto RUNTIME         = get_index_v<Runtime, handle_t>;
+  constexpr static auto SUBFLOW         = get_index_v<Subflow, handle_t>;
+  constexpr static auto CONDITION       = get_index_v<Condition, handle_t>;
+  constexpr static auto MULTI_CONDITION = get_index_v<MultiCondition, handle_t>;
+  constexpr static auto MODULE          = get_index_v<Module, handle_t>;
+  constexpr static auto ASYNC           = get_index_v<Async, handle_t>;
+  constexpr static auto DEPENDENT_ASYNC = get_index_v<DependentAsync, handle_t>;
+
+  Node() = default;
+  
+  template <typename... Args>
+  Node(const std::string&, Topology*, Node*, size_t, Args&&...);
+  
+  template <typename... Args>
+  Node(nstate_t, estate_t, const std::string&, Topology*, Node*, size_t, Args&&...);
+  
+  template <typename... Args>
+  Node(const TaskParams&, Topology*, Node*, size_t, Args&&...);
+  
+  template <typename... Args>
+  Node(nstate_t, estate_t, const TaskParams&, Topology*, Node*, size_t, Args&&...);
+  
+  template <typename... Args>
+  Node(const DefaultTaskParams&, Topology*, Node*, size_t, Args&&...);
+  
+  template <typename... Args>
+  Node(nstate_t, estate_t, const DefaultTaskParams&, Topology*, Node*, size_t, Args&&...);
+
+  //~Node();
+
+  size_t num_successors() const;
+  size_t num_dependents() const;
+  size_t num_strong_dependents() const;
+  size_t num_weak_dependents() const;
+
+  const std::string& name() const;
+
+  private:
+  
+  nstate_t _nstate              {NSTATE::NONE};
+  std::atomic<estate_t> _estate {ESTATE::NONE};
+
+  std::string _name;
+  
+  void* _data {nullptr};
+  
+  Topology* _topology {nullptr};
+  Node* _parent {nullptr};
+
+  SmallVector<Node*> _successors;
+  SmallVector<Node*> _dependents;
+
+  std::atomic<size_t> _join_counter {0};
+  
+  handle_t _handle;
+  
+  std::unique_ptr<Semaphores> _semaphores;
+  
+  std::exception_ptr _exception_ptr {nullptr};
+
+  // free list
+  //Node* _freelist_next{nullptr};
+
+  bool _is_cancelled() const;
+  bool _is_conditioner() const;
+  bool _is_preempted() const;
+  bool _acquire_all(SmallVector<Node*>&);
+  void _release_all(SmallVector<Node*>&);
+  void _precede(Node*);
+  void _set_up_join_counter();
+  void _rethrow_exception();
+};
+
+// ----------------------------------------------------------------------------
+// Node Object Pool
+// ----------------------------------------------------------------------------
+
+/**
+@private
+*/
+#ifdef TF_ENABLE_TASK_POOL
+inline ObjectPool<Node> _task_pool;
+#endif
+
+/**
+@private
+*/
+template <typename... ArgsT>
+TF_FORCE_INLINE Node* animate(ArgsT&&... args) {
+#ifdef TF_ENABLE_TASK_POOL
+  return _task_pool.animate(std::forward<ArgsT>(args)...);
+#else
+  return new Node(std::forward<ArgsT>(args)...);
+#endif
+}
+
+/**
+@private
+*/
+TF_FORCE_INLINE void recycle(Node* ptr) {
+#ifdef TF_ENABLE_TASK_POOL
+  _task_pool.recycle(ptr);
+#else
+  delete ptr;
+#endif
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::Static
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::Static::Static(C&& c) : work {std::forward<C>(c)} {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::Runtime
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::Runtime::Runtime(C&& c) : work {std::forward<C>(c)} {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::Subflow
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::Subflow::Subflow(C&& c) : work {std::forward<C>(c)} {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::Condition
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::Condition::Condition(C&& c) : work {std::forward<C>(c)} {
+}                                        
+
+// ----------------------------------------------------------------------------
+// Definition for Node::MultiCondition
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::MultiCondition::MultiCondition(C&& c) : work {std::forward<C>(c)} {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::Module
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename T>
+inline Node::Module::Module(T& obj) : graph{ obj.graph() } {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::Async
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::Async::Async(C&& c) : work {std::forward<C>(c)} {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node::DependentAsync
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename C>
+Node::DependentAsync::DependentAsync(C&& c) : work {std::forward<C>(c)} {
+}
+
+// ----------------------------------------------------------------------------
+// Definition for Node
+// ----------------------------------------------------------------------------
+
+// Constructor
+template <typename... Args>
+Node::Node(
+  const std::string& name,
+  Topology* topology, 
+  Node* parent, 
+  size_t join_counter,
+  Args&&... args
+) :
+  _name         {name},
+  _topology     {topology},
+  _parent       {parent},
+  _join_counter {join_counter},
+  _handle       {std::forward<Args>(args)...} {
+}
+
+// Constructor
+template <typename... Args>
+Node::Node(
+  nstate_t nstate,
+  estate_t estate,
+  const std::string& name,
+  Topology* topology, 
+  Node* parent, 
+  size_t join_counter,
+  Args&&... args
+) :
+  _nstate       {nstate},
+  _estate       {estate},
+  _name         {name},
+  _topology     {topology},
+  _parent       {parent},
+  _join_counter {join_counter},
+  _handle       {std::forward<Args>(args)...} {
+}
+
+// Constructor
+template <typename... Args>
+Node::Node(
+  const TaskParams& params,
+  Topology* topology, 
+  Node* parent, 
+  size_t join_counter,
+  Args&&... args
+) :
+  _name         {params.name},
+  _data         {params.data},
+  _topology     {topology},
+  _parent       {parent},
+  _join_counter {join_counter},
+  _handle       {std::forward<Args>(args)...} {
+}
+
+// Constructor
+template <typename... Args>
+Node::Node(
+  nstate_t nstate,
+  estate_t estate,
+  const TaskParams& params,
+  Topology* topology, 
+  Node* parent, 
+  size_t join_counter,
+  Args&&... args
+) :
+  _nstate       {nstate},
+  _estate       {estate},
+  _name         {params.name},
+  _data         {params.data},
+  _topology     {topology},
+  _parent       {parent},
+  _join_counter {join_counter},
+  _handle       {std::forward<Args>(args)...} {
+}
+
+// Constructor
+template <typename... Args>
+Node::Node(
+  const DefaultTaskParams&,
+  Topology* topology, 
+  Node* parent, 
+  size_t join_counter,
+  Args&&... args
+) :
+  _topology     {topology},
+  _parent       {parent},
+  _join_counter {join_counter},
+  _handle       {std::forward<Args>(args)...} {
+}
+
+// Constructor
+template <typename... Args>
+Node::Node(
+  nstate_t nstate,
+  estate_t estate,
+  const DefaultTaskParams&,
+  Topology* topology, 
+  Node* parent, 
+  size_t join_counter,
+  Args&&... args
+) :
+  _nstate       {nstate},
+  _estate       {estate},
+  _topology     {topology},
+  _parent       {parent},
+  _join_counter {join_counter},
+  _handle       {std::forward<Args>(args)...} {
+}
+
+// Destructor
+//inline Node::~Node() {
+//  // this is to avoid stack overflow
+//  if(_handle.index() == SUBFLOW) {
+//    auto& subgraph = std::get_if<Subflow>(&_handle)->subgraph;
+//    std::vector<Node*> nodes;
+//    nodes.reserve(subgraph.size());
+//
+//    std::move(
+//      subgraph._nodes.begin(), subgraph._nodes.end(), std::back_inserter(nodes)
+//    );
+//    subgraph._nodes.clear();
+//
+//    size_t i = 0;
+//
+//    while(i < nodes.size()) {
+//
+//      if(nodes[i]->_handle.index() == SUBFLOW) {
+//        auto& sbg = std::get_if<Subflow>(&(nodes[i]->_handle))->subgraph;
+//        std::move(
+//          sbg._nodes.begin(), sbg._nodes.end(), std::back_inserter(nodes)
+//        );
+//        sbg._nodes.clear();
+//      }
+//
+//      ++i;
+//    }
+//
+//    //auto& np = Graph::_node_pool();
+//    for(i=0; i<nodes.size(); ++i) {
+//      recycle(nodes[i]);
+//    }
+//  }
+//}
+
+// Procedure: _precede
+inline void Node::_precede(Node* v) {
+  _successors.push_back(v);
+  v->_dependents.push_back(this);
+}
+
+// Function: num_successors
+inline size_t Node::num_successors() const {
+  return _successors.size();
+}
+
+// Function: dependents
+inline size_t Node::num_dependents() const {
+  return _dependents.size();
+}
+
+// Function: num_weak_dependents
+inline size_t Node::num_weak_dependents() const {
+  size_t n = 0;
+  for(size_t i=0; i<_dependents.size(); i++) {
+    if(_dependents[i]->_is_conditioner()) {
+      n++;
+    }
+  }
+  return n;
+}
+
+// Function: num_strong_dependents
+inline size_t Node::num_strong_dependents() const {
+  size_t n = 0;
+  for(size_t i=0; i<_dependents.size(); i++) {
+    if(!_dependents[i]->_is_conditioner()) {
+      n++;
+    }
+  }
+  return n;
+}
+
+// Function: name
+inline const std::string& Node::name() const {
+  return _name;
+}
+
+// Function: _is_conditioner
+inline bool Node::_is_conditioner() const {
+  return _handle.index() == Node::CONDITION ||
+         _handle.index() == Node::MULTI_CONDITION;
+}
+
+// Function: _is_preempted
+inline bool Node::_is_preempted() const {
+  return _nstate & NSTATE::PREEMPTED;
+}
+
+// Function: _is_cancelled
+// we currently only support cancellation of taskflow (no async task)
+inline bool Node::_is_cancelled() const {
+  return (_topology && (_topology->_estate.load(std::memory_order_relaxed) & ESTATE::CANCELLED)) 
+         ||
+         (_parent && (_parent->_estate.load(std::memory_order_relaxed) & ESTATE::CANCELLED));
+}
+
+// Procedure: _set_up_join_counter
+inline void Node::_set_up_join_counter() {
+  size_t c = 0;
+  for(auto p : _dependents) {
+    if(p->_is_conditioner()) {
+      //_nstate |= NSTATE::CONDITIONED;
+      _nstate = (_nstate + 1) | NSTATE::CONDITIONED;
+    }
+    else {
+      c++;
+    }
+  }
+  _join_counter.store(c, std::memory_order_relaxed);
+}
+
+// Procedure: _rethrow_exception
+inline void Node::_rethrow_exception() {
+  if(_exception_ptr) {
+    auto e = _exception_ptr;
+    _exception_ptr = nullptr;
+    std::rethrow_exception(e);
+  }
+}
+
+// Function: _acquire_all
+inline bool Node::_acquire_all(SmallVector<Node*>& nodes) {
+  // assert(_semaphores != nullptr);
+  auto& to_acquire = _semaphores->to_acquire;
+  for(size_t i = 0; i < to_acquire.size(); ++i) {
+    if(!to_acquire[i]->_try_acquire_or_wait(this)) {
+      for(size_t j = 1; j <= i; ++j) {
+        to_acquire[i-j]->_release(nodes);
+      }
+      return false;
+    }
+  }
+  return true;
+}
+
+// Function: _release_all
+inline void Node::_release_all(SmallVector<Node*>& nodes) {
+  // assert(_semaphores != nullptr);
+  auto& to_release = _semaphores->to_release;
+  for(const auto& sem : to_release) {
+    sem->_release(nodes);
+  }
+}
+
+
+
+// ----------------------------------------------------------------------------
+// AnchorGuard
+// ----------------------------------------------------------------------------
+
+/**
+@private
+*/
+class AnchorGuard {
+
+  public:
+  
+  // anchor is at estate as it may be accessed by multiple threads (e.g., corun's
+  // parent with tear_down_async's parent).
+  AnchorGuard(Node* node) : _node{node} { 
+    _node->_estate.fetch_or(ESTATE::ANCHORED, std::memory_order_relaxed);
+  }
+
+  ~AnchorGuard() {
+    _node->_estate.fetch_and(~ESTATE::ANCHORED, std::memory_order_relaxed);
+  }
+  
+  private:
+
+  Node* _node;
+};
+
+
+// ----------------------------------------------------------------------------
+// Graph definition
+// ----------------------------------------------------------------------------
+
+// Function: erase
+inline void Graph::_erase(Node* node) {
+  erase(
+    std::remove_if(begin(), end(), [&](auto& p){ return p.get() == node; }),
+    end()
+  );
+}
+
+/**
+@private
+*/
+template <typename ...ArgsT>
+Node* Graph::_emplace_back(ArgsT&&... args) {
+  push_back(std::make_unique<Node>(std::forward<ArgsT>(args)...));
+  return back().get();
+}
+
+// ----------------------------------------------------------------------------
+// Graph checker
+// ----------------------------------------------------------------------------
+
+/**
+@private
+ */
+template <typename T, typename = void>
+struct has_graph : std::false_type {};
+
+/**
+@private
+ */
+template <typename T>
+struct has_graph<T, std::void_t<decltype(std::declval<T>().graph())>>
+    : std::is_same<decltype(std::declval<T>().graph()), Graph&> {};
+
+/**
+ * @brief determines if the given type has a member function `Graph& graph()`
+ *
+ * This trait determines if the provided type `T` contains a member function
+ * with the exact signature `tf::Graph& graph()`. It uses SFINAE and `std::void_t`
+ * to detect the presence of the member function and its return type.
+ *
+ * @tparam T The type to inspect.
+ * @retval true If the type `T` has a member function `tf::Graph& graph()`.
+ * @retval false Otherwise.
+ *
+ * Example usage:
+ * @code
+ *
+ * struct A {
+ *   tf::Graph& graph() { return my_graph; };
+ *   tf::Graph my_graph;
+ *
+ *   // other custom members to alter my_graph
+ * };
+ *
+ * struct C {}; // No graph function
+ *
+ * static_assert(has_graph_v<A>, "A has graph()");
+ * static_assert(!has_graph_v<C>, "C does not have graph()");
+ * @endcode
+ */
+template <typename T>
+constexpr bool has_graph_v = has_graph<T>::value;
+
+// ----------------------------------------------------------------------------
+// detailed helper functions
+// ----------------------------------------------------------------------------
+
+namespace detail {
+
+/**
+@private
+*/
+template <typename T>
+TF_FORCE_INLINE Node* get_node_ptr(T& node) {
+  using U = std::decay_t<T>;
+  if constexpr (std::is_same_v<U, Node*>) {
+    return node;
+  } 
+  else if constexpr (std::is_same_v<U, std::unique_ptr<Node>>) {
+    return node.get();
+  } 
+  else {
+    static_assert(dependent_false_v<T>, "Unsupported type for get_node_ptr");
+  }
+} 
+
+}  // end of namespace tf::detail ---------------------------------------------
+
+
+}  // end of namespace tf. ----------------------------------------------------
+
+
+
similarity index 51%
rename from bundled/taskflow-3.7.0/taskflow/core/notifier.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/nonblocking_notifier.hpp
index 61663798a0c4f999bbe0d769453096a975b10421..bd356c672539be2ff196939e5dc6d90318945763 100644 (file)
@@ -1,6 +1,3 @@
-// 2019/02/09 - created by Tsung-Wei Huang
-//  - modified the event count from Eigen
-
 #pragma once
 
 #include <iostream>
@@ -49,10 +46,10 @@ namespace tf {
 //   ec.notify(true);
 //
 // notify is cheap if there are no waiting threads. prepare_wait/commit_wait are not
-// cheap, but they are executed only if the preceeding predicate check has
+// cheap, but they are executed only if the preceding predicate check has
 // failed.
 //
-// Algorihtm outline:
+// Algorithm outline:
 // There are two main variables: predicate (managed by user) and _state.
 // Operation closely resembles Dekker mutual algorithm:
 // https://en.wikipedia.org/wiki/Dekker%27s_algorithm
@@ -62,14 +59,15 @@ namespace tf {
 // and won't block, or notifying thread will see _state change and will unblock
 // the waiter, or both. But it can't happen that both threads don't see each
 // other changes, which would lead to deadlock.
-class Notifier {
+
+class NonblockingNotifierV1 {
 
   friend class Executor;
 
   public:
 
   struct Waiter {
-    std::atomic<Waiter*> next;
+    alignas (2*TF_CACHELINE_SIZE) std::atomic<Waiter*> next;
     uint64_t epoch;
     enum : unsigned {
       kNotSignaled = 0,
@@ -77,7 +75,7 @@ class Notifier {
       kSignaled,
     };
 
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
     std::atomic<unsigned> state {0};
 #else
     std::mutex mu;
@@ -86,13 +84,13 @@ class Notifier {
 #endif
   };
 
-  explicit Notifier(size_t N) : _waiters{N} {
+  explicit NonblockingNotifierV1(size_t N) : _state(kStackMask), _waiters(N) {
     assert(_waiters.size() < (1 << kWaiterBits) - 1);
     // Initialize epoch to something close to overflow to test overflow.
-    _state = kStackMask | (kEpochMask - kEpochInc * _waiters.size() * 2);
+    //_state = kStackMask | (kEpochMask - kEpochInc * _waiters.size() * 2);
   }
 
-  ~Notifier() {
+  ~NonblockingNotifierV1() {
     // Ensure there are no waiters.
     assert((_state.load() & (kStackMask | kWaiterMask)) == kStackMask);
   }
@@ -108,7 +106,7 @@ class Notifier {
   // commit_wait commits waiting.
   // only the waiter itself can call
   void commit_wait(Waiter* w) {
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
     w->state.store(Waiter::kNotSignaled, std::memory_order_relaxed);
 #else
     w->state = Waiter::kNotSignaled;
@@ -120,7 +118,7 @@ class Notifier {
     uint64_t state = _state.load(std::memory_order_seq_cst);
     for (;;) {
       if (int64_t((state & kEpochMask) - epoch) < 0) {
-        // The preceeding waiter has not decided on its fate. Wait until it
+        // The preceding waiter has not decided on its fate. Wait until it
         // calls either cancel_wait or commit_wait, or is notified.
         std::this_thread::yield();
         state = _state.load(std::memory_order_seq_cst);
@@ -152,7 +150,7 @@ class Notifier {
     uint64_t state = _state.load(std::memory_order_relaxed);
     for (;;) {
       if (int64_t((state & kEpochMask) - epoch) < 0) {
-        // The preceeding waiter has not decided on its fate. Wait until it
+        // The preceding waiter has not decided on its fate. Wait until it
         // calls either cancel_wait or commit_wait, or is notified.
         std::this_thread::yield();
         state = _state.load(std::memory_order_relaxed);
@@ -168,9 +166,96 @@ class Notifier {
     }
   }
 
+  void notify_one() {
+    _notify<false>();
+  }
+
+  void notify_all() {
+    _notify<true>();
+  }
+
+  // notify n workers
+  void notify_n(size_t n) {
+    if(n >= _waiters.size()) {
+      _notify<true>();
+    }
+    else {
+      for(size_t k=0; k<n; ++k) {
+        _notify<false>();
+      }
+    }
+  }
+
+  size_t size() const {
+    return _waiters.size();
+  }
+
+ private:
+
+  // State_ layout:
+  // - low kStackBits is a stack of waiters committed wait.
+  // - next kWaiterBits is count of waiters in prewait state.
+  // - next kEpochBits is modification counter.
+  static const uint64_t kStackBits = 16;
+  static const uint64_t kStackMask = (1ull << kStackBits) - 1;
+  static const uint64_t kWaiterBits = 16;
+  static const uint64_t kWaiterShift = 16;
+  static const uint64_t kWaiterMask = ((1ull << kWaiterBits) - 1)
+                                      << kWaiterShift;
+  static const uint64_t kWaiterInc = 1ull << kWaiterBits;
+  static const uint64_t kEpochBits = 32;
+  static const uint64_t kEpochShift = 32;
+  static const uint64_t kEpochMask = ((1ull << kEpochBits) - 1) << kEpochShift;
+  static const uint64_t kEpochInc = 1ull << kEpochShift;
+  std::atomic<uint64_t> _state;
+  std::vector<Waiter> _waiters;
+
+  void _park(Waiter* w) {
+#if __cplusplus >= TF_CPP20
+    unsigned target = Waiter::kNotSignaled;
+    if(w->state.compare_exchange_strong(target, Waiter::kWaiting,
+                                        std::memory_order_relaxed,
+                                        std::memory_order_relaxed)) {
+      w->state.wait(Waiter::kWaiting, std::memory_order_relaxed);
+    }
+#else
+    std::unique_lock<std::mutex> lock(w->mu);
+    while (w->state != Waiter::kSignaled) {
+      w->state = Waiter::kWaiting;
+      w->cv.wait(lock);
+    }
+#endif
+  }
+
+  void _unpark(Waiter* waiters) {
+    Waiter* next = nullptr;
+    for (Waiter* w = waiters; w; w = next) {
+      next = w->next.load(std::memory_order_relaxed);
+#if __cplusplus >= TF_CPP20
+      // We only notify if the other is waiting - this is why we use tri-state
+      // variable instead of binary-state variable (i.e., atomic_flag)
+      // Performance is about 0.1% faster
+      if(w->state.exchange(Waiter::kSignaled, std::memory_order_relaxed) == 
+         Waiter::kWaiting) {
+        w->state.notify_one();
+      }
+#else
+      unsigned state;
+      {
+        std::unique_lock<std::mutex> lock(w->mu);
+        state = w->state;
+        w->state = Waiter::kSignaled;
+      }
+      // Avoid notifying if it wasn't waiting.
+      if (state == Waiter::kWaiting) w->cv.notify_one();
+#endif
+    }
+  }
+  
   // notify wakes one or all waiting threads.
   // Must be called after changing the associated wait predicate.
-  void notify(bool all) {
+  template <bool all>
+  void _notify() {
     std::atomic_thread_fence(std::memory_order_seq_cst);
     uint64_t state = _state.load(std::memory_order_acquire);
     for (;;) {
@@ -214,15 +299,122 @@ class Notifier {
       }
     }
   }
+};
+
+
+// ----------------------------------------------------------------------------
+// NonblockingNotifierV2
+// reference: https://gitlab.com/libeigen/eigen/-/blob/master/Eigen/src/ThreadPool/EventCount.h
+// ----------------------------------------------------------------------------
+class NonblockingNotifierV2 {
+
+  friend class Executor;
+
+  public:
+  
+  struct Waiter {
+    alignas (2*TF_CACHELINE_SIZE) std::atomic<uint64_t> next;
+    uint64_t epoch;
+    enum : unsigned {
+      kNotSignaled = 0,
+      kWaiting,
+      kSignaled,
+    };
+
+#if __cplusplus >= TF_CPP20
+    std::atomic<unsigned> state {0};
+#else
+    std::mutex mu;
+    std::condition_variable cv;
+    unsigned state;
+#endif
+  };
+
+  explicit NonblockingNotifierV2(size_t N) : _state(kStackMask), _waiters(N) {
+    assert(N < ((1 << kWaiterBits) - 1));
+  }
+
+  ~NonblockingNotifierV2() {
+    // Ensure there are no waiters.
+    assert(_state.load() == kStackMask);
+  }
+
+  // prepare_wait prepares for waiting.
+  // After calling prepare_wait, the thread must re-check the wait predicate
+  // and then call either cancel_wait or commit_wait.
+  void prepare_wait(Waiter*) {
+    uint64_t state = _state.load(std::memory_order_relaxed);
+    for (;;) {
+      //_check_state(state);
+      uint64_t newstate = state + kWaiterInc;
+      //_check_state(newstate);
+      if (_state.compare_exchange_weak(state, newstate, std::memory_order_seq_cst)) return;
+    }
+  }
+
+  // commit_wait commits waiting after prepare_wait.
+  void commit_wait(Waiter* w) {
+#if __cplusplus >= TF_CPP20
+    w->state.store(Waiter::kNotSignaled, std::memory_order_relaxed);
+#else
+    w->state = Waiter::kNotSignaled;
+#endif
+    const uint64_t me = (w - &_waiters[0]) | w->epoch;
+    uint64_t state = _state.load(std::memory_order_seq_cst);
+    for (;;) {
+      //_check_state(state, true);
+      uint64_t newstate;
+      if ((state & kSignalMask) != 0) {
+        // Consume the signal and return immediately.
+        newstate = state - kWaiterInc - kSignalInc;
+      } else {
+        // Remove this thread from pre-wait counter and add to the waiter stack.
+        newstate = ((state & kWaiterMask) - kWaiterInc) | me;
+        w->next.store(state & (kStackMask | kEpochMask), std::memory_order_relaxed);
+      }
+      //_check_state(newstate);
+      if (_state.compare_exchange_weak(state, newstate, std::memory_order_acq_rel)) {
+        if ((state & kSignalMask) == 0) {
+          w->epoch += kEpochInc;
+          _park(w);
+        }
+        return;
+      }
+    }
+  }
+
+  // cancel_wait cancels effects of the previous prepare_wait call.
+  void cancel_wait(Waiter*) {
+    uint64_t state = _state.load(std::memory_order_relaxed);
+    for (;;) {
+      //_check_state(state, true);
+      uint64_t newstate = state - kWaiterInc;
+      // We don't know if the thread was also notified or not,
+      // so we should not consume a signal unconditionally.
+      // Only if number of waiters is equal to number of signals,
+      // we know that the thread was notified and we must take away the signal.
+      if (((state & kWaiterMask) >> kWaiterShift) == ((state & kSignalMask) >> kSignalShift)) newstate -= kSignalInc;
+      //_check_state(newstate);
+      if (_state.compare_exchange_weak(state, newstate, std::memory_order_acq_rel)) return;
+    }
+  }
+
+  void notify_one() {
+    _notify<false>();
+  }
 
+  void notify_all() {
+    _notify<true>();
+  }
+  
   // notify n workers
   void notify_n(size_t n) {
     if(n >= _waiters.size()) {
-      notify(true);
+      _notify<true>();
     }
     else {
       for(size_t k=0; k<n; ++k) {
-        notify(false);
+        _notify<false>();
       }
     }
   }
@@ -231,28 +423,35 @@ class Notifier {
     return _waiters.size();
   }
 
- private:
-
+  private:
   // State_ layout:
-  // - low kStackBits is a stack of waiters committed wait.
+  // - low kWaiterBits is a stack of waiters committed wait
+  //   (indexes in _waiters array are used as stack elements,
+  //   kStackMask means empty stack).
   // - next kWaiterBits is count of waiters in prewait state.
-  // - next kEpochBits is modification counter.
-  static const uint64_t kStackBits = 16;
-  static const uint64_t kStackMask = (1ull << kStackBits) - 1;
-  static const uint64_t kWaiterBits = 16;
-  static const uint64_t kWaiterShift = 16;
-  static const uint64_t kWaiterMask = ((1ull << kWaiterBits) - 1)
-                                      << kWaiterShift;
-  static const uint64_t kWaiterInc = 1ull << kWaiterBits;
-  static const uint64_t kEpochBits = 32;
-  static const uint64_t kEpochShift = 32;
+  // - next kWaiterBits is count of pending signals.
+  // - remaining bits are ABA counter for the stack.
+  //   (stored in Waiter node and incremented on push).
+  static const uint64_t kWaiterBits = 14;
+  static const uint64_t kStackMask = (1ull << kWaiterBits) - 1;
+  static const uint64_t kWaiterShift = kWaiterBits;
+  static const uint64_t kWaiterMask = ((1ull << kWaiterBits) - 1) << kWaiterShift;
+  static const uint64_t kWaiterInc = 1ull << kWaiterShift;
+  static const uint64_t kSignalShift = 2 * kWaiterBits;
+  static const uint64_t kSignalMask = ((1ull << kWaiterBits) - 1) << kSignalShift;
+  static const uint64_t kSignalInc = 1ull << kSignalShift;
+  static const uint64_t kEpochShift = 3 * kWaiterBits;
+  static const uint64_t kEpochBits = 64 - kEpochShift;
   static const uint64_t kEpochMask = ((1ull << kEpochBits) - 1) << kEpochShift;
   static const uint64_t kEpochInc = 1ull << kEpochShift;
+  
+  static_assert(kEpochBits >= 20, "not enough bits to prevent ABA problem");
+
   std::atomic<uint64_t> _state;
   std::vector<Waiter> _waiters;
 
   void _park(Waiter* w) {
-#ifdef __cpp_lib_atomic_wait
+#if __cplusplus >= TF_CPP20
     unsigned target = Waiter::kNotSignaled;
     if(w->state.compare_exchange_strong(target, Waiter::kWaiting,
                                         std::memory_order_relaxed,
@@ -268,19 +467,16 @@ class Notifier {
 #endif
   }
 
-  void _unpark(Waiter* waiters) {
-    Waiter* next = nullptr;
-    for (Waiter* w = waiters; w; w = next) {
-      next = w->next.load(std::memory_order_relaxed);
-#ifdef __cpp_lib_atomic_wait
-      // We only notify if the other is waiting - this is why we use tri-state
-      // variable instead of binary-state variable (i.e., atomic_flag)
-      // Performance is about 0.1% faster
+  void _unpark(Waiter* w) {
+    for (Waiter* next; w; w = next) {
+      uint64_t wnext = w->next.load(std::memory_order_relaxed) & kStackMask;
+      next = (wnext == kStackMask) ? nullptr : &_waiters[static_cast<size_t>(wnext)];
+#if __cplusplus >= TF_CPP20
       if(w->state.exchange(Waiter::kSignaled, std::memory_order_relaxed) == 
          Waiter::kWaiting) {
         w->state.notify_one();
       }
-#else
+#else      
       unsigned state;
       {
         std::unique_lock<std::mutex> lock(w->mu);
@@ -292,10 +488,59 @@ class Notifier {
 #endif
     }
   }
+  
+  // Notify wakes one or all waiting threads.
+  // Must be called after changing the associated wait predicate.
+  template <bool notifyAll>
+  void _notify() {
+    std::atomic_thread_fence(std::memory_order_seq_cst);
+    uint64_t state = _state.load(std::memory_order_acquire);
+    for (;;) {
+      //_check_state(state);
+      const uint64_t waiters = (state & kWaiterMask) >> kWaiterShift;
+      const uint64_t signals = (state & kSignalMask) >> kSignalShift;
+      // Easy case: no waiters.
+      if ((state & kStackMask) == kStackMask && waiters == signals) return;
+      uint64_t newstate;
+      if (notifyAll) {
+        // Empty wait stack and set signal to number of pre-wait threads.
+        newstate = (state & kWaiterMask) | (waiters << kSignalShift) | kStackMask;
+      } else if (signals < waiters) {
+        // There is a thread in pre-wait state, unblock it.
+        newstate = state + kSignalInc;
+      } else {
+        // Pop a waiter from list and unpark it.
+        Waiter* w = &_waiters[state & kStackMask];
+        uint64_t next = w->next.load(std::memory_order_relaxed);
+        newstate = (state & (kWaiterMask | kSignalMask)) | next;
+      }
+      //_check_state(newstate);
+      if (_state.compare_exchange_weak(state, newstate, std::memory_order_acq_rel)) {
+        if (!notifyAll && (signals < waiters)) return;  // unblocked pre-wait thread
+        if ((state & kStackMask) == kStackMask) return;
+        Waiter* w = &_waiters[state & kStackMask];
+        if (!notifyAll) w->next.store(kStackMask, std::memory_order_relaxed);
+        _unpark(w);
+        return;
+      }
+    }
+  }
 
-};
+  //static void _check_state(uint64_t state, bool waiter = false) {
+  //  const uint64_t waiters = (state & kWaiterMask) >> kWaiterShift;
+  //  const uint64_t signals = (state & kSignalMask) >> kSignalShift;
+  //  assert(waiters >= signals);
+  //  assert(waiters < (1 << kWaiterBits) - 1);
+  //  assert(!waiter || waiters > 0);
+  //  (void)waiters;
+  //  (void)signals;
+  //}
 
 
+  NonblockingNotifierV2(const NonblockingNotifierV2&) = delete;
+  void operator=(const NonblockingNotifierV2&) = delete;
+};
+
 
 }  // namespace tf ------------------------------------------------------------
 
diff --git a/bundled/taskflow-3.10.0/taskflow/core/runtime.hpp b/bundled/taskflow-3.10.0/taskflow/core/runtime.hpp
new file mode 100644 (file)
index 0000000..a60d1dc
--- /dev/null
@@ -0,0 +1,604 @@
+#pragma once
+
+#include "executor.hpp"
+
+namespace tf {
+
+/**
+@class Runtime
+
+@brief class to include a runtime object in a task
+
+A runtime object allows users to interact with the
+scheduling runtime inside a task, such as scheduling an active task,
+spawning a subflow, and so on.
+
+@code{.cpp}
+tf::Task A, B, C, D;
+std::tie(A, B, C, D) = taskflow.emplace(
+  [] () { return 0; },
+  [&C] (tf::Runtime& rt) {  // C must be captured by reference
+    std::cout << "B\n";
+    rt.schedule(C);
+  },
+  [] () { std::cout << "C\n"; },
+  [] () { std::cout << "D\n"; }
+);
+A.precede(B, C, D);
+executor.run(taskflow).wait();
+@endcode
+
+A runtime object is associated with the worker and the executor
+that runs the task.
+
+*/
+class Runtime {
+
+  friend class Executor;
+  friend class FlowBuilder;
+  friend class PreemptionGuard;
+  friend class Algorithm;
+  
+  #define TF_RUNTIME_CHECK_CALLER(msg)                                          \
+  if(pt::this_worker == nullptr || pt::this_worker->_executor != &_executor) {  \
+    TF_THROW(msg);                                                              \
+  }
+
+  public:
+  
+  /**
+  @brief obtains the running executor
+
+  The running executor of a runtime task is the executor that runs
+  the parent taskflow of that runtime task.
+
+  @code{.cpp}
+  tf::Executor executor;
+  tf::Taskflow taskflow;
+  taskflow.emplace([&](tf::Runtime& rt){
+    assert(&(rt.executor()) == &executor);
+  });
+  executor.run(taskflow).wait();
+  @endcode
+  */
+  Executor& executor();
+  
+  /**
+  @brief acquire a reference to the underlying worker
+  */
+  inline Worker& worker();
+
+  /**
+  @brief schedules an active task immediately to the worker's queue
+
+  @param task the given active task to schedule immediately
+
+  This member function immediately schedules an active task to the
+  task queue of the associated worker in the runtime task.
+  An active task is a task in a running taskflow.
+  The task may or may not be running, and scheduling that task
+  will immediately put the task into the task queue of the worker
+  that is running the runtime task.
+  Consider the following example:
+
+  @code{.cpp}
+  tf::Task A, B, C, D;
+  std::tie(A, B, C, D) = taskflow.emplace(
+    [] () { return 0; },
+    [&C] (tf::Runtime& rt) {  // C must be captured by reference
+      std::cout << "B\n";
+      rt.schedule(C);
+    },
+    [] () { std::cout << "C\n"; },
+    [] () { std::cout << "D\n"; }
+  );
+  A.precede(B, C, D);
+  executor.run(taskflow).wait();
+  @endcode
+
+  The executor will first run the condition task @c A which returns @c 0
+  to inform the scheduler to go to the runtime task @c B.
+  During the execution of @c B, it directly schedules task @c C without
+  going through the normal taskflow graph scheduling process.
+  At this moment, task @c C is active because its parent taskflow is running.
+  When the taskflow finishes, we will see both @c B and @c C in the output.
+  */
+  void schedule(Task task);
+  
+  /**
+  @brief runs the given callable asynchronously
+
+  @tparam F callable type
+  @param f callable object
+    
+  The method creates an asynchronous task to launch the given
+  function on the given arguments.
+  The difference to tf::Executor::async is that the created asynchronous task
+  pertains to the runtime object.
+  Applications can explicitly issue tf::Runtime::corun_all
+  to wait for all spawned asynchronous tasks to finish.
+  For example:
+
+  @code{.cpp}
+  std::atomic<int> counter(0);
+  taskflow.emplace([&](tf::Runtime& rt){
+    auto fu1 = rt.async([&](){ counter++; });
+    auto fu2 = rt.async([&](){ counter++; });
+    fu1.get();
+    fu2.get();
+    assert(counter == 2);
+    
+    // spawn 100 asynchronous tasks from the worker of the runtime
+    for(int i=0; i<100; i++) {
+      rt.async([&](){ counter++; });
+    }
+    
+    // wait for the 100 asynchronous tasks to finish
+    rt.corun_all();
+    assert(counter == 102);
+  });
+  @endcode
+
+  This method is thread-safe and can be called by multiple workers
+  that hold the reference to the runtime.
+  For example, the code below spawns 100 tasks from the worker of
+  a runtime, and each of the 100 tasks spawns another task
+  that will be run by another worker.
+  
+  @code{.cpp}
+  std::atomic<int> counter(0);
+  taskflow.emplace([&](tf::Runtime& rt){
+    // worker of the runtime spawns 100 tasks each spawning another task
+    // that will be run by another worker
+    for(int i=0; i<100; i++) {
+      rt.async([&](){ 
+        counter++; 
+        rt.async([](){ counter++; });
+      });
+    }
+    
+    // wait for the 200 asynchronous tasks to finish
+    rt.corun_all();
+    assert(counter == 200);
+  });
+  @endcode
+  */
+  template <typename F>
+  auto async(F&& f);
+  
+  /**
+  @brief runs the given callable asynchronously
+
+  @tparam F callable type
+  @tparam P task parameters type
+
+  @param params task parameters
+  @param f callable
+
+  <p><!-- Doxygen warning workaround --></p>
+
+  @code{.cpp}
+  taskflow.emplace([&](tf::Runtime& rt){
+    auto future = rt.async("my task", [](){});
+    future.get();
+  });
+  @endcode
+
+  */
+  template <typename P, typename F>
+  auto async(P&& params, F&& f);
+
+  /**
+  @brief runs the given function asynchronously without returning any future object
+
+  @tparam F callable type
+  @param f callable
+
+  This member function is more efficient than tf::Runtime::async
+  and is encouraged to use when there is no data returned.
+
+  @code{.cpp}
+  std::atomic<int> counter(0);
+  taskflow.emplace([&](tf::Runtime& rt){
+    for(int i=0; i<100; i++) {
+      rt.silent_async([&](){ counter++; });
+    }
+    rt.corun_all();
+    assert(counter == 100);
+  });
+  @endcode
+
+  This member function is thread-safe.
+  */
+  template <typename F>
+  void silent_async(F&& f);
+  
+  /**
+  @brief runs the given function asynchronously without returning any future object
+
+  @tparam F callable type
+  @param params task parameters
+  @param f callable
+
+  <p><!-- Doxygen warning workaround --></p>
+
+  @code{.cpp}
+  taskflow.emplace([&](tf::Runtime& rt){
+    rt.silent_async("my task", [](){});
+    rt.corun_all();
+  });
+  @endcode
+  */
+  template <typename P, typename F>
+  void silent_async(P&& params, F&& f);
+  
+  /**
+  @brief co-runs the given target and waits until it completes
+  
+  A corunnable target must have `tf::Graph& T::graph()` defined.
+
+  // co-run a taskflow and wait until all tasks complete
+  @code{.cpp}
+  tf::Taskflow taskflow1, taskflow2;
+  taskflow1.emplace([](){ std::cout << "running taskflow1\n"; });
+  taskflow2.emplace([&](tf::Runtime& rt){
+    std::cout << "running taskflow2\n";
+    rt.corun(taskflow1);
+  });
+  executor.run(taskflow2).wait();
+  @endcode
+
+  Although tf::Runtime::corun blocks until the operation completes, 
+  the caller thread (worker) is not blocked (e.g., sleeping or holding any lock).
+  Instead, the caller thread joins the work-stealing loop of the executor 
+  and returns when all tasks in the target completes.
+  
+  @attention
+  The method is not thread-safe as it modifies the anchor state of the node for exception handling.
+  */
+  template <typename T>
+  void corun(T&& target);
+
+  /**
+  @brief corun all asynchronous tasks spawned by this runtime with other workers
+
+  Coruns all asynchronous tasks (tf::Runtime::async,
+  tf::Runtime::silent_async) with other workers until all those 
+  asynchronous tasks finish.
+    
+  @code{.cpp}
+  std::atomic<size_t> counter{0};
+  taskflow.emplace([&](tf::Runtime& rt){
+    // spawn 100 async tasks and wait
+    for(int i=0; i<100; i++) {
+      rt.silent_async([&](){ counter++; });
+    }
+    rt.corun_all();
+    assert(counter == 100);
+    
+    // spawn another 100 async tasks and wait
+    for(int i=0; i<100; i++) {
+      rt.silent_async([&](){ counter++; });
+    }
+    rt.corun_all();
+    assert(counter == 200);
+  });
+  @endcode
+
+  @attention
+  The method is not thread-safe as it modifies the anchor state of the node for exception handling.
+  */
+  inline void corun_all();
+
+  protected:
+  
+  /**
+  @private
+  */
+  explicit Runtime(Executor&, Worker&, Node*);
+  
+  /**
+  @private
+  */
+  Executor& _executor;
+  
+  /**
+  @private
+  */
+  Worker& _worker;
+  
+  /**
+  @private
+  */
+  Node* _parent;
+  
+  /**
+  @private
+  */
+  bool _preempted {false};
+};
+
+// constructor
+inline Runtime::Runtime(Executor& executor, Worker& worker, Node* parent) :
+  _executor {executor},
+  _worker   {worker},
+  _parent   {parent} {
+}
+
+// Function: executor
+inline Executor& Runtime::executor() {
+  return _executor;
+}
+
+// Function: worker
+inline Worker& Runtime::worker() {
+  return _worker;
+}
+
+// Procedure: schedule
+inline void Runtime::schedule(Task task) {
+  
+  TF_RUNTIME_CHECK_CALLER("schedule must be called by a worker of runtime's executor");
+  
+  auto node = task._node;
+  // need to keep the invariant: when scheduling a task, the task must have
+  // zero dependency (join counter is 0)
+  // or we can encounter bug when inserting a nested flow (e.g., module task)
+  node->_join_counter.store(0, std::memory_order_relaxed);
+
+  auto& j = node->_parent ? node->_parent->_join_counter :
+                            node->_topology->_join_counter;
+  j.fetch_add(1, std::memory_order_relaxed);
+  _executor._schedule(_worker, node);
+}
+
+// Procedure: corun
+template <typename T>
+void Runtime::corun(T&& target) {
+
+  static_assert(has_graph_v<T>, "target must define a member function 'Graph& graph()'");
+
+  TF_RUNTIME_CHECK_CALLER("corun must be called by a worker of runtime's executor");
+  _executor._corun_graph(*pt::this_worker, _parent, target.graph().begin(), target.graph().end());
+}
+
+// Function: corun_all
+inline void Runtime::corun_all() {
+  TF_RUNTIME_CHECK_CALLER("corun_all must be called by a worker of runtime's executor");
+  {
+    AnchorGuard anchor(_parent);
+    _executor._corun_until(_worker, [this] () -> bool {
+      return _parent->_join_counter.load(std::memory_order_acquire) == 0;
+    });
+  }
+  _parent->_rethrow_exception();
+}
+
+// ------------------------------------
+// Runtime::silent_async series
+// ------------------------------------
+
+// Function: silent_async
+template <typename F>
+void Runtime::silent_async(F&& f) {
+  silent_async(DefaultTaskParams{}, std::forward<F>(f));
+}
+
+// Function: silent_async
+template <typename P, typename F>
+void Runtime::silent_async(P&& params, F&& f) {
+  _parent->_join_counter.fetch_add(1, std::memory_order_relaxed);
+  _executor._silent_async(
+    std::forward<P>(params), std::forward<F>(f), _parent->_topology, _parent
+  );
+}
+
+// ------------------------------------
+// Runtime::async series
+// ------------------------------------
+
+// Function: async
+template <typename F>
+auto Runtime::async(F&& f) {
+  return async(DefaultTaskParams{}, std::forward<F>(f));
+}
+
+// Function: async
+template <typename P, typename F>
+auto Runtime::async(P&& params, F&& f) {
+  _parent->_join_counter.fetch_add(1, std::memory_order_relaxed);
+  return _executor._async(
+    std::forward<P>(params), std::forward<F>(f), _parent->_topology, _parent
+  );
+}
+
+// ----------------------------------------------------------------------------
+// Preemption guard
+// ----------------------------------------------------------------------------
+
+/**
+@private
+*/
+class PreemptionGuard {
+
+  public:
+
+  PreemptionGuard(Runtime& runtime) : _runtime {runtime} {
+    if(_runtime._preempted == true) {
+      TF_THROW("runtime is not preemptible");
+    }
+    _runtime._parent->_nstate |= NSTATE::PREEMPTED;
+    _runtime._preempted = true;
+    _runtime._parent->_join_counter.fetch_add(1, std::memory_order_release);
+  }
+
+  ~PreemptionGuard() {
+    if(_runtime._parent->_join_counter.fetch_sub(1, std::memory_order_acq_rel) == 1) {
+      _runtime._preempted = false;
+      _runtime._parent->_nstate &= ~NSTATE::PREEMPTED;
+    }
+  }
+
+  PreemptionGuard(const PreemptionGuard&) = delete;
+  PreemptionGuard(PreemptionGuard&&) = delete;
+
+  PreemptionGuard& operator = (const PreemptionGuard&) = delete;
+  PreemptionGuard& operator = (PreemptionGuard&&) = delete;
+  
+  private:
+
+  Runtime& _runtime;
+};
+
+
+// ----------------------------------------------------------------------------
+// Executor Forward Declaration
+// ----------------------------------------------------------------------------
+
+// Procedure: _invoke_runtime_task
+inline bool Executor::_invoke_runtime_task(Worker& worker, Node* node) {
+  return _invoke_runtime_task_impl(
+    worker, node, std::get_if<Node::Runtime>(&node->_handle)->work
+  );
+}
+
+// Function: _invoke_runtime_task_impl
+inline bool Executor::_invoke_runtime_task_impl(
+  Worker& worker, Node* node, std::function<void(Runtime&)>& work
+) {
+  // first time
+  if((node->_nstate & NSTATE::PREEMPTED) == 0) {
+
+    Runtime rt(*this, worker, node);
+
+    _observer_prologue(worker, node);
+    TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
+      work(rt);
+    });
+    _observer_epilogue(worker, node);
+    
+    // here, we cannot check the state from node->_nstate due to data race
+    if(rt._preempted) {
+      return true;
+    }
+  }
+  // second time - previously preempted
+  else {
+    node->_nstate &= ~NSTATE::PREEMPTED;
+  }
+  return false;
+}
+
+// Function: _invoke_runtime_task_impl
+inline bool Executor::_invoke_runtime_task_impl(
+  Worker& worker, Node* node, std::function<void(Runtime&, bool)>& work
+) {
+    
+  Runtime rt(*this, worker, node);
+
+  // first time
+  if((node->_nstate & NSTATE::PREEMPTED) == 0) {
+
+    _observer_prologue(worker, node);
+    TF_EXECUTOR_EXCEPTION_HANDLER(worker, node, {
+      work(rt, false);
+    });
+    _observer_epilogue(worker, node);
+    
+    // here, we cannot check the state from node->_nstate due to data race
+    if(rt._preempted) {
+      return true;
+    }
+  }
+  // second time - previously preempted
+  else {
+    node->_nstate &= ~NSTATE::PREEMPTED;
+  }
+
+  // clean up outstanding work
+  work(rt, true);
+
+  return false;
+}
+
+
+// ----------------------------------------------------------------------------
+// Executor Members that Depend on Runtime
+// ----------------------------------------------------------------------------
+
+template <typename P, typename F>
+auto Executor::_async(P&& params, F&& f, Topology* tpg, Node* parent) {
+  
+  // async task with runtime: [] (tf::Runtime&) { ... }
+  if constexpr (is_runtime_task_v<F>) {
+
+    std::promise<void> p;
+    auto fu{p.get_future()};
+    
+    _schedule_async_task(animate(
+      NSTATE::NONE, ESTATE::ANCHORED, std::forward<P>(params), tpg, parent, 0, 
+      std::in_place_type_t<Node::Async>{}, 
+      [p=MoC{std::move(p)}, f=std::forward<F>(f)](Runtime& rt, bool reentered) mutable { 
+        if(!reentered) {
+          f(rt);
+        }
+        else {
+          auto& eptr = rt._parent->_exception_ptr;
+          eptr ? p.object.set_exception(eptr) : p.object.set_value();
+        }
+      }
+    ));
+    return fu;
+  }
+  // async task with closure: [] () { ... }
+  else if constexpr (std::is_invocable_v<F>){
+    std::packaged_task p(std::forward<F>(f));
+    auto fu{p.get_future()};
+    _schedule_async_task(animate(
+      std::forward<P>(params), tpg, parent, 0, 
+      std::in_place_type_t<Node::Async>{}, 
+      [p=make_moc(std::move(p))]() mutable { p.object(); }
+    ));
+    return fu;
+  }
+  else {
+    static_assert(dependent_false_v<F>, 
+      "invalid async target - must be one of the following types:\n\
+      (1) [] (tf::Runtime&) -> void {}\n\
+      (2) [] () -> auto { ... return ... }\n"
+    );
+  }
+
+}
+
+// Function: _silent_async
+template <typename P, typename F>
+void Executor::_silent_async(P&& params, F&& f, Topology* tpg, Node* parent) {
+  // silent task 
+  if constexpr (is_runtime_task_v<F> || std::is_invocable_v<F>) {
+    _schedule_async_task(animate(
+      std::forward<P>(params), tpg, parent, 0,
+      std::in_place_type_t<Node::Async>{}, std::forward<F>(f)
+    ));
+  }
+  // invalid silent async target
+  else {
+    static_assert(dependent_false_v<F>, 
+      "invalid silent_async target - must be one of the following types:\n\
+      (1) [] (tf::Runtime&) -> void {}\n\
+      (2) [] () -> auto { ... return ... }\n"
+    );
+  }
+}
+
+
+}  // end of namespace tf -----------------------------------------------------
+
+
+
+
+
+
+
+
+
similarity index 58%
rename from bundled/taskflow-3.7.0/taskflow/core/semaphore.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/semaphore.hpp
index 12d6069b1d24a595d62f3fe465a7b50b96db401d..f7502e982207adbc56283d2ba71825fac64cef78 100644 (file)
@@ -1,9 +1,9 @@
 #pragma once
 
-#include <vector>
 #include <mutex>
 
 #include "declarations.hpp"
+#include "../utility/small_vector.hpp"
 
 /**
 @file semaphore.hpp
@@ -41,7 +41,7 @@ tf::Taskflow taskflow;
 
 tf::Semaphore semaphore(1); // create a semaphore with initial count 1
 
-std::vector<tf::Task> tasks {
+SmallVector<tf::Task> tasks {
   taskflow.emplace([](){ std::cout << "A" << std::endl; }),
   taskflow.emplace([](){ std::cout << "B" << std::endl; }),
   taskflow.emplace([](){ std::cout << "C" << std::endl; }),
@@ -68,11 +68,20 @@ This arrangement limits the number of concurrently running tasks to only one.
 class Semaphore {
 
   friend class Node;
+  friend class Executor;
 
   public:
 
     /**
-    @brief constructs a semaphore with the given counter
+    @brief constructs a default semaphore
+
+    A default semaphore has the value of zero. Users can call tf::Semaphore::reset
+    to reassign a new value to the semaphore.
+    */
+    Semaphore() = default;
+
+    /**
+    @brief constructs a semaphore with the given value (i.e., counter)
 
     A semaphore creates a constraint that limits the maximum concurrency,
     i.e., the number of workers, in a set of tasks.
@@ -81,34 +90,51 @@ class Semaphore {
     tf::Semaphore semaphore(4);  // concurrency constraint of 4 workers
     @endcode
     */
-    explicit Semaphore(size_t max_workers);
+    explicit Semaphore(size_t max_value);
 
     /**
-    @brief queries the counter value (not thread-safe during the run)
+    @brief queries the current counter value
     */
-    size_t count() const;
+    size_t value() const;
 
-  private:
+    /**
+    @brief queries the maximum allowable value of this semaphore
+    */
+    size_t max_value() const;
 
-    std::mutex _mtx;
+    /**
+    @brief resets the semaphores to a clean state
+    */
+    void reset();
+    
+    /**
+    @brief resets the semaphores to a clean state with the given new maximum value
+    */
+    void reset(size_t new_max_value);
 
-    size_t _counter;
+  private:
 
-    std::vector<Node*> _waiters;
+    mutable std::mutex _mtx;
+    
+    size_t _max_value{0};
+    size_t _cur_value{0};
+
+    SmallVector<Node*> _waiters;
 
     bool _try_acquire_or_wait(Node*);
 
-    std::vector<Node*> _release();
+    void _release(SmallVector<Node*>&);
 };
 
-inline Semaphore::Semaphore(size_t max_workers) :
-  _counter(max_workers) {
+inline Semaphore::Semaphore(size_t max_value) :
+  _max_value(max_value),
+  _cur_value(max_value) {
 }
 
 inline bool Semaphore::_try_acquire_or_wait(Node* me) {
   std::lock_guard<std::mutex> lock(_mtx);
-  if(_counter > 0) {
-    --_counter;
+  if(_cur_value > 0) {
+    --_cur_value;
     return true;
   }
   else {
@@ -117,15 +143,45 @@ inline bool Semaphore::_try_acquire_or_wait(Node* me) {
   }
 }
 
-inline std::vector<Node*> Semaphore::_release() {
+inline void Semaphore::_release(SmallVector<Node*>& dst) {
+
   std::lock_guard<std::mutex> lock(_mtx);
-  ++_counter;
-  std::vector<Node*> r{std::move(_waiters)};
-  return r;
+
+  if(_cur_value >= _max_value) {
+    TF_THROW("can't release the semaphore more than its maximum value: ", _max_value);
+  }
+
+  ++_cur_value;
+  
+  if(dst.empty()) {
+    dst.swap(_waiters);
+  }
+  else {
+    dst.reserve(dst.size() + _waiters.size());
+    dst.insert(dst.end(), _waiters.begin(), _waiters.end());
+    _waiters.clear();
+  }
+}
+
+inline size_t Semaphore::max_value() const {
+  return _max_value; 
 }
 
-inline size_t Semaphore::count() const {
-  return _counter;
+inline size_t Semaphore::value() const {
+  std::lock_guard<std::mutex> lock(_mtx);
+  return _cur_value;
+}
+
+inline void Semaphore::reset() {
+  std::lock_guard<std::mutex> lock(_mtx);
+  _cur_value = _max_value;
+  _waiters.clear();
+}
+
+inline void Semaphore::reset(size_t new_max_value) {
+  std::lock_guard<std::mutex> lock(_mtx);
+  _cur_value = (_max_value = new_max_value);
+  _waiters.clear();
 }
 
 }  // end of namespace tf. ---------------------------------------------------
similarity index 88%
rename from bundled/taskflow-3.7.0/taskflow/core/task.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/task.hpp
index 1070671c4097553d46c195531a6d88d1a41cdd8f..46a15b95df8153a2f9b58bfc31546776b330e4db 100644 (file)
@@ -23,6 +23,8 @@ enum class TaskType : int {
   PLACEHOLDER = 0,
   /** @brief static task type */
   STATIC,
+  /** @brief runtime task type */
+  RUNTIME,
   /** @brief dynamic (subflow) task type */
   SUBFLOW,
   /** @brief condition task type */
@@ -39,9 +41,10 @@ enum class TaskType : int {
 @private
 @brief array of all task types (used for iterating task types)
 */
-inline constexpr std::array<TaskType, 6> TASK_TYPES = {
+inline constexpr std::array<TaskType, 7> TASK_TYPES = {
   TaskType::PLACEHOLDER,
   TaskType::STATIC,
+  TaskType::RUNTIME,
   TaskType::SUBFLOW,
   TaskType::CONDITION,
   TaskType::MODULE,
@@ -56,6 +59,7 @@ The name of each task type is the litte-case string of its characters.
 @code{.cpp}
 TaskType::PLACEHOLDER     ->  "placeholder"
 TaskType::STATIC          ->  "static"
+TaskType::RUNTIME         ->  "runtime"
 TaskType::SUBFLOW         ->  "subflow"
 TaskType::CONDITION       ->  "condition"
 TaskType::MODULE          ->  "module"
@@ -69,6 +73,7 @@ inline const char* to_string(TaskType type) {
   switch(type) {
     case TaskType::PLACEHOLDER:      val = "placeholder";     break;
     case TaskType::STATIC:           val = "static";          break;
+    case TaskType::RUNTIME:          val = "runtime";         break;
     case TaskType::SUBFLOW:          val = "subflow";         break;
     case TaskType::CONDITION:        val = "condition";       break;
     case TaskType::MODULE:           val = "module";          break;
@@ -89,46 +94,42 @@ inline const char* to_string(TaskType type) {
 A dynamic task is a callable object constructible from std::function<void(Subflow&)>.
 */
 template <typename C>
-constexpr bool is_subflow_task_v = 
-  std::is_invocable_r_v<void, C, Subflow&> &&
-  !std::is_invocable_r_v<void, C, Runtime&>;
+constexpr bool is_subflow_task_v = std::is_invocable_r_v<void, C, Subflow&>;
 
 /**
 @brief determines if a callable is a condition task
 
-A condition task is a callable object constructible from std::function<int()>
-or std::function<int(tf::Runtime&)>.
+A condition task is a callable object constructible from std::function<int()>.
 */
 template <typename C>
-constexpr bool is_condition_task_v = 
-  (std::is_invocable_r_v<int, C> || std::is_invocable_r_v<int, C, Runtime&>) &&
-  !is_subflow_task_v<C>;
+constexpr bool is_condition_task_v = std::is_invocable_r_v<int, C>;
 
 /**
 @brief determines if a callable is a multi-condition task
 
 A multi-condition task is a callable object constructible from
-std::function<tf::SmallVector<int>()> or
-std::function<tf::SmallVector<int>(tf::Runtime&)>.
+std::function<tf::SmallVector<int>()>.
 */
 template <typename C>
-constexpr bool is_multi_condition_task_v =
-  (std::is_invocable_r_v<SmallVector<int>, C> ||
-  std::is_invocable_r_v<SmallVector<int>, C, Runtime&>) &&
-  !is_subflow_task_v<C>;
+constexpr bool is_multi_condition_task_v = std::is_invocable_r_v<SmallVector<int>, C>;
 
 /**
 @brief determines if a callable is a static task
 
-A static task is a callable object constructible from std::function<void()>
-or std::function<void(tf::Runtime&)>.
+A static task is a callable object constructible from std::function<void()>.
 */
 template <typename C>
-constexpr bool is_static_task_v =
-  (std::is_invocable_r_v<void, C> || std::is_invocable_r_v<void, C, Runtime&>) &&
-  !is_condition_task_v<C> &&
-  !is_multi_condition_task_v<C> &&
-  !is_subflow_task_v<C>;
+constexpr bool is_static_task_v = std::is_invocable_r_v<void, C> &&
+                                  !is_condition_task_v<C>        &&
+                                  !is_multi_condition_task_v<C>;
+
+/**
+@brief determines if a callable is a runtime task
+
+A runtime task is a callable object constructible from std::function<void(tf::Runtime&)>.
+*/
+template <typename C>
+constexpr bool is_runtime_task_v = std::is_invocable_r_v<void, C, Runtime&>;
 
 // ----------------------------------------------------------------------------
 // Task
@@ -271,12 +272,24 @@ class Task {
     @brief makes the task release this semaphore
     */
     Task& release(Semaphore& semaphore);
+    
+    /**
+    @brief makes the task release the given range of semaphores
+    */
+    template <typename I>
+    Task& release(I first, I last);
 
     /**
     @brief makes the task acquire this semaphore
     */
     Task& acquire(Semaphore& semaphore);
 
+    /**
+    @brief makes the task acquire the given range of semaphores
+    */
+    template <typename I>
+    Task& acquire(I first, I last);
+
     /**
     @brief assigns pointer to user data
 
@@ -308,22 +321,6 @@ class Task {
     */
     Task& data(void* data);
     
-    /**
-    @brief assigns a priority value to the task
-
-    A priority value can be one of the following three levels, 
-    tf::TaskPriority::HIGH (numerically equivalent to 0),
-    tf::TaskPriority::NORMAL (numerically equivalent to 1), and
-    tf::TaskPriority::LOW (numerically equivalent to 2).
-    The smaller the priority value, the higher the priority.
-    */
-    Task& priority(TaskPriority p);
-    
-    /**
-    @brief queries the priority value of the task
-    */
-    TaskPriority priority() const;
-
     /**
     @brief resets the task handle to null
     */
@@ -452,16 +449,45 @@ inline Task& Task::acquire(Semaphore& s) {
   return *this;
 }
 
+// Function: acquire
+template <typename I>
+Task& Task::acquire(I first, I last) {
+  if(!_node->_semaphores) {
+    _node->_semaphores = std::make_unique<Node::Semaphores>();
+  }
+  _node->_semaphores->to_acquire.reserve(
+    _node->_semaphores->to_acquire.size() + std::distance(first, last)
+  );
+  for(auto s = first; s != last; ++s){
+    _node->_semaphores->to_acquire.push_back(&(*s));
+  }
+  return *this;
+}
+
 // Function: release
 inline Task& Task::release(Semaphore& s) {
   if(!_node->_semaphores) {
-    //_node->_semaphores.emplace();
     _node->_semaphores = std::make_unique<Node::Semaphores>();
   }
   _node->_semaphores->to_release.push_back(&s);
   return *this;
 }
 
+// Function: release
+template <typename I>
+Task& Task::release(I first, I last) {
+  if(!_node->_semaphores) {
+    _node->_semaphores = std::make_unique<Node::Semaphores>();
+  }
+  _node->_semaphores->to_release.reserve(
+    _node->_semaphores->to_release.size() + std::distance(first, last)
+  );
+  for(auto s = first; s != last; ++s) {
+    _node->_semaphores->to_release.push_back(&(*s));
+  }
+  return *this;
+}
+
 // Procedure: reset
 inline void Task::reset() {
   _node = nullptr;
@@ -512,6 +538,7 @@ inline TaskType Task::type() const {
   switch(_node->_handle.index()) {
     case Node::PLACEHOLDER:     return TaskType::PLACEHOLDER;
     case Node::STATIC:          return TaskType::STATIC;
+    case Node::RUNTIME:         return TaskType::RUNTIME;
     case Node::SUBFLOW:         return TaskType::SUBFLOW;
     case Node::CONDITION:       return TaskType::CONDITION;
     case Node::MULTI_CONDITION: return TaskType::CONDITION;
@@ -558,6 +585,9 @@ Task& Task::work(C&& c) {
   if constexpr(is_static_task_v<C>) {
     _node->_handle.emplace<Node::Static>(std::forward<C>(c));
   }
+  else if constexpr(is_runtime_task_v<C>) {
+    _node->_handle.emplace<Node::Runtime>(std::forward<C>(c));
+  }
   else if constexpr(is_subflow_task_v<C>) {
     _node->_handle.emplace<Node::Subflow>(std::forward<C>(c));
   }
@@ -584,17 +614,6 @@ inline Task& Task::data(void* data) {
   return *this;
 }
 
-// Function: priority
-inline Task& Task::priority(TaskPriority p) {
-  _node->_priority = static_cast<unsigned>(p);
-  return *this;
-}
-
-// Function: priority
-inline TaskPriority Task::priority() const {
-  return static_cast<TaskPriority>(_node->_priority);
-}
-
 // ----------------------------------------------------------------------------
 // global ostream
 // ----------------------------------------------------------------------------
@@ -711,6 +730,7 @@ inline TaskType TaskView::type() const {
   switch(_node._handle.index()) {
     case Node::PLACEHOLDER:     return TaskType::PLACEHOLDER;
     case Node::STATIC:          return TaskType::STATIC;
+    case Node::RUNTIME:         return TaskType::RUNTIME;
     case Node::SUBFLOW:         return TaskType::SUBFLOW;
     case Node::CONDITION:       return TaskType::CONDITION;
     case Node::MULTI_CONDITION: return TaskType::CONDITION;
similarity index 97%
rename from bundled/taskflow-3.7.0/taskflow/core/taskflow.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/taskflow.hpp
index f6a0f424a2334c8534437ae74ffa1fb284877c5b..09d4ffdc04163c4e62e3484df53f72875fa6dc09 100644 (file)
@@ -69,6 +69,7 @@ class Taskflow : public FlowBuilder {
   friend class Topology;
   friend class Executor;
   friend class FlowBuilder;
+  friend class Subflow;
 
   struct Dumper {
     size_t id;
@@ -251,7 +252,9 @@ class Taskflow : public FlowBuilder {
 
     @param from from task (dependent)
     @param to to task (successor)
-  
+
+    <p><!-- Doxygen warning workaround --></p>
+
     @code{.cpp}
     tf::Taskflow taskflow;
     auto a = taskflow.placeholder().name("a");
@@ -335,7 +338,7 @@ inline Taskflow& Taskflow::operator = (Taskflow&& rhs) {
 
 // Procedure:
 inline void Taskflow::clear() {
-  _graph._clear();
+  _graph.clear();
 }
 
 // Function: num_tasks
@@ -366,8 +369,8 @@ inline Graph& Taskflow::graph() {
 // Function: for_each_task
 template <typename V>
 void Taskflow::for_each_task(V&& visitor) const {
-  for(size_t i=0; i<_graph._nodes.size(); ++i) {
-    visitor(Task(_graph._nodes[i]));
+  for(auto itr = _graph.begin(); itr != _graph.end(); ++itr) {
+    visitor(Task(itr->get()));
   }
 }
 
@@ -439,12 +442,13 @@ inline void Taskflow::_dump(
   std::ostream& os, const Node* node, Dumper& dumper
 ) const {
 
+  // label of the node
   os << 'p' << node << "[label=\"";
   if(node->_name.empty()) os << 'p' << node;
   else os << node->_name;
   os << "\" ";
 
-  // shape for node
+  // shape of the node
   switch(node->_handle.index()) {
 
     case Node::CONDITION:
@@ -472,7 +476,7 @@ inline void Taskflow::_dump(
   if(node->_parent && node->_parent->_handle.index() == Node::SUBFLOW &&
      node->_successors.size() == 0
     ) {
-    os << 'p' << node << " -> p" << node->_parent << ";\n";
+    os << 'p' << node << " -> p" << node->_parent << " [style=dashed color=blue];\n";
   }
 
   // node info
@@ -502,7 +506,9 @@ inline void Taskflow::_dump(
   std::ostream& os, const Graph* graph, Dumper& dumper
 ) const {
 
-  for(const auto& n : graph->_nodes) {
+  for(auto itr = graph->begin(); itr != graph->end(); ++itr) {
+
+    Node* n = itr->get();
 
     // regular task
     if(n->_handle.index() != Node::MODULE) {
@@ -633,7 +639,7 @@ Future<T>::Future(std::future<T>&& f, std::weak_ptr<Topology> p) :
 template <typename T>
 bool Future<T>::cancel() {
   if(auto ptr = _topology.lock(); ptr) {
-    ptr->_state.fetch_or(Topology::CANCELLED, std::memory_order_relaxed);
+    ptr->_estate.fetch_or(ESTATE::CANCELLED, std::memory_order_relaxed);
     return true;
   }
   return false;
similarity index 83%
rename from bundled/taskflow-3.7.0/taskflow/core/topology.hpp
rename to bundled/taskflow-3.10.0/taskflow/core/topology.hpp
index 335ccfb80a8c9b712ef35d84e09904a255e30863..354b72c69fd5c4f4ec92344d26e097e0bf7278db 100644 (file)
@@ -12,16 +12,13 @@ class TopologyBase {
 class Topology {
 
   friend class Executor;
+  friend class Subflow;
   friend class Runtime;
   friend class Node;
 
   template <typename T>
   friend class Future;
   
-  constexpr static int CLEAN = 0;
-  constexpr static int CANCELLED = 1;
-  constexpr static int EXCEPTION = 2;
-
   public:
 
     template <typename P, typename C>
@@ -34,14 +31,12 @@ class Topology {
     Taskflow& _taskflow;
 
     std::promise<void> _promise;
-
-    SmallVector<Node*> _sources;
-
+    
     std::function<bool()> _pred;
     std::function<void()> _call;
 
     std::atomic<size_t> _join_counter {0};
-    std::atomic<int> _state {CLEAN};
+    std::atomic<ESTATE::underlying_type> _estate {ESTATE::NONE};
 
     std::exception_ptr _exception_ptr {nullptr};
 
@@ -70,7 +65,7 @@ inline void Topology::_carry_out_promise() {
 
 // Function: cancelled
 inline bool Topology::cancelled() const {
-  return _state.load(std::memory_order_relaxed) & CANCELLED;
+  return _estate.load(std::memory_order_relaxed) & ESTATE::CANCELLED;
 }
 
 }  // end of namespace tf. ----------------------------------------------------
diff --git a/bundled/taskflow-3.10.0/taskflow/core/tsq.hpp b/bundled/taskflow-3.10.0/taskflow/core/tsq.hpp
new file mode 100644 (file)
index 0000000..10e9e12
--- /dev/null
@@ -0,0 +1,727 @@
+#pragma once
+
+#include "../utility/macros.hpp"
+#include "../utility/traits.hpp"
+
+/**
+@file tsq.hpp
+@brief task queue include file
+*/
+
+#ifndef TF_DEFAULT_BOUNDED_TASK_QUEUE_LOG_SIZE 
+  /**
+  @def TF_DEFAULT_BOUNDED_TASK_QUEUE_LOG_SIZE
+  
+  This macro defines the default size of the bounded task queue in Log2. 
+  Bounded task queue is used by each worker.
+  */
+  #define TF_DEFAULT_BOUNDED_TASK_QUEUE_LOG_SIZE 8
+#endif
+
+#ifndef TF_DEFAULT_UNBOUNDED_TASK_QUEUE_LOG_SIZE 
+  /**
+  @def TF_DEFAULT_UNBOUNDED_TASK_QUEUE_LOG_SIZE
+  
+  This macro defines the default size of the unbounded task queue in Log2.
+  Unbounded task queue is used by the executor.
+  */
+  #define TF_DEFAULT_UNBOUNDED_TASK_QUEUE_LOG_SIZE 10
+#endif
+
+namespace tf {
+
+// ----------------------------------------------------------------------------
+// Task Queue
+// ----------------------------------------------------------------------------
+
+
+/**
+@class: UnboundedTaskQueue
+
+@tparam T data type (must be a pointer type)
+
+@brief class to create a lock-free unbounded work-stealing queue
+
+This class implements the work-stealing queue described in the paper,
+<a href="https://www.di.ens.fr/~zappa/readings/ppopp13.pdf">Correct and Efficient Work-Stealing for Weak Memory Models</a>.
+
+Only the queue owner can perform pop and push operations,
+while others can steal data from the queue simultaneously.
+
+*/
+template <typename T>
+class UnboundedTaskQueue {
+  
+  static_assert(std::is_pointer_v<T>, "T must be a pointer type");
+
+  struct Array {
+
+    int64_t C;
+    int64_t M;
+    std::atomic<T>* S;
+
+    explicit Array(int64_t c) :
+      C {c},
+      M {c-1},
+      S {new std::atomic<T>[static_cast<size_t>(C)]} {
+    }
+
+    ~Array() {
+      delete [] S;
+    }
+
+    int64_t capacity() const noexcept {
+      return C;
+    }
+
+    void push(int64_t i, T o) noexcept {
+      S[i & M].store(o, std::memory_order_relaxed);
+    }
+
+    T pop(int64_t i) noexcept {
+      return S[i & M].load(std::memory_order_relaxed);
+    }
+
+    Array* resize(int64_t b, int64_t t) {
+      Array* ptr = new Array {2*C};
+      for(int64_t i=t; i!=b; ++i) {
+        ptr->push(i, pop(i));
+      }
+      return ptr;
+    }
+
+  };
+
+  // Doubling the alignment by 2 seems to generate the most
+  // decent performance.
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<int64_t> _top;
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<int64_t> _bottom;
+  std::atomic<Array*> _array;
+  std::vector<Array*> _garbage;
+
+  public:
+
+  /**
+  @brief constructs the queue with the given size in the base-2 logarithm
+
+  @param LogSize the base-2 logarithm of the queue size
+  */
+  explicit UnboundedTaskQueue(int64_t LogSize = TF_DEFAULT_UNBOUNDED_TASK_QUEUE_LOG_SIZE);
+
+  /**
+  @brief destructs the queue
+  */
+  ~UnboundedTaskQueue();
+
+  /**
+  @brief queries if the queue is empty at the time of this call
+  */
+  bool empty() const noexcept;
+
+  /**
+  @brief queries the number of items at the time of this call
+  */
+  size_t size() const noexcept;
+
+  /**
+  @brief queries the capacity of the queue
+  */
+  int64_t capacity() const noexcept;
+  
+  /**
+  @brief inserts an item to the queue
+
+  @param item the item to push to the queue
+  
+  Only the owner thread can insert an item to the queue.
+  The operation can trigger the queue to resize its capacity
+  if more space is required.
+  */
+  void push(T item);
+
+  /**
+  @brief pops out an item from the queue
+
+  Only the owner thread can pop out an item from the queue.
+  The return can be a @c nullptr if this operation failed (empty queue).
+  */
+  T pop();
+
+  /**
+  @brief steals an item from the queue
+
+  Any threads can try to steal an item from the queue.
+  The return can be a @c nullptr if this operation failed (not necessary empty).
+  */
+  T steal();
+
+  private:
+
+  Array* resize_array(Array* a, int64_t b, int64_t t);
+};
+
+// Constructor
+template <typename T>
+UnboundedTaskQueue<T>::UnboundedTaskQueue(int64_t LogSize) {
+  _top.store(0, std::memory_order_relaxed);
+  _bottom.store(0, std::memory_order_relaxed);
+  _array.store(new Array{(int64_t{1} << LogSize)}, std::memory_order_relaxed);
+  _garbage.reserve(32);
+}
+
+// Destructor
+template <typename T>
+UnboundedTaskQueue<T>::~UnboundedTaskQueue() {
+  for(auto a : _garbage) {
+    delete a;
+  }
+  delete _array.load();
+}
+
+// Function: empty
+template <typename T>
+bool UnboundedTaskQueue<T>::empty() const noexcept {
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_relaxed);
+  return (b <= t);
+}
+
+// Function: size
+template <typename T>
+size_t UnboundedTaskQueue<T>::size() const noexcept {
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_relaxed);
+  return static_cast<size_t>(b >= t ? b - t : 0);
+}
+
+// Function: push
+template <typename T>
+void UnboundedTaskQueue<T>::push(T o) {
+
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_acquire);
+  Array* a = _array.load(std::memory_order_relaxed);
+
+  // queue is full
+  if TF_UNLIKELY(a->capacity() - 1 < (b - t)) {
+    a = resize_array(a, b, t);
+  }
+
+  a->push(b, o);
+  std::atomic_thread_fence(std::memory_order_release);
+
+  // original paper uses relaxed here but tsa complains
+  _bottom.store(b + 1, std::memory_order_release);
+}
+
+// Function: pop
+template <typename T>
+T UnboundedTaskQueue<T>::pop() {
+
+  int64_t b = _bottom.load(std::memory_order_relaxed) - 1;
+  Array* a = _array.load(std::memory_order_relaxed);
+  _bottom.store(b, std::memory_order_relaxed);
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+  int64_t t = _top.load(std::memory_order_relaxed);
+
+  T item {nullptr};
+
+  if(t <= b) {
+    item = a->pop(b);
+    if(t == b) {
+      // the last item just got stolen
+      if(!_top.compare_exchange_strong(t, t+1,
+                                               std::memory_order_seq_cst,
+                                               std::memory_order_relaxed)) {
+        item = nullptr;
+      }
+      _bottom.store(b + 1, std::memory_order_relaxed);
+    }
+  }
+  else {
+    _bottom.store(b + 1, std::memory_order_relaxed);
+  }
+
+  return item;
+}
+
+// Function: steal
+template <typename T>
+T UnboundedTaskQueue<T>::steal() {
+  
+  int64_t t = _top.load(std::memory_order_acquire);
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+  int64_t b = _bottom.load(std::memory_order_acquire);
+
+  T item {nullptr};
+
+  if(t < b) {
+    Array* a = _array.load(std::memory_order_consume);
+    item = a->pop(t);
+    if(!_top.compare_exchange_strong(t, t+1,
+                                     std::memory_order_seq_cst,
+                                     std::memory_order_relaxed)) {
+      return nullptr;
+    }
+  }
+
+  return item;
+}
+
+// Function: capacity
+template <typename T>
+int64_t UnboundedTaskQueue<T>::capacity() const noexcept {
+  return _array.load(std::memory_order_relaxed)->capacity();
+}
+
+template <typename T>
+typename UnboundedTaskQueue<T>::Array*
+UnboundedTaskQueue<T>::resize_array(Array* a, int64_t b, int64_t t) {
+
+  Array* tmp = a->resize(b, t);
+  _garbage.push_back(a);
+  std::swap(a, tmp);
+  _array.store(a, std::memory_order_release);
+  // Note: the original paper using relaxed causes t-san to complain
+  //_array.store(a, std::memory_order_relaxed);
+  return a;
+}
+
+// ----------------------------------------------------------------------------
+// BoundedTaskQueue
+// ----------------------------------------------------------------------------
+
+/**
+@class: BoundedTaskQueue
+
+@tparam T data type
+@tparam LogSize the base-2 logarithm of the queue size
+
+@brief class to create a lock-free bounded work-stealing queue
+
+This class implements the work-stealing queue described in the paper, 
+"Correct and Efficient Work-Stealing for Weak Memory Models,"
+available at https://www.di.ens.fr/~zappa/readings/ppopp13.pdf.
+
+Only the queue owner can perform pop and push operations,
+while others can steal data from the queue.
+*/
+template <typename T, size_t LogSize = TF_DEFAULT_BOUNDED_TASK_QUEUE_LOG_SIZE>
+class BoundedTaskQueue {
+  
+  static_assert(std::is_pointer_v<T>, "T must be a pointer type");
+  
+  constexpr static int64_t BufferSize = int64_t{1} << LogSize;
+  constexpr static int64_t BufferMask = (BufferSize - 1);
+
+  static_assert((BufferSize >= 2) && ((BufferSize & (BufferSize - 1)) == 0));
+
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<int64_t> _top {0};
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<int64_t> _bottom {0};
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<T> _buffer[BufferSize];
+
+  public:
+    
+  /**
+  @brief constructs the queue with a given capacity
+  */
+  BoundedTaskQueue() = default;
+
+  /**
+  @brief destructs the queue
+  */
+  ~BoundedTaskQueue() = default;
+  
+  /**
+  @brief queries if the queue is empty at the time of this call
+  */
+  bool empty() const noexcept;
+  
+  /**
+  @brief queries the number of items at the time of this call
+  */
+  size_t size() const noexcept;
+
+  /**
+  @brief queries the capacity of the queue
+  */
+  constexpr size_t capacity() const;
+  
+  /**
+  @brief tries to insert an item to the queue
+
+  @tparam O data type 
+  @param item the item to perfect-forward to the queue
+  @return `true` if the insertion succeed or `false` (queue is full)
+  
+  Only the owner thread can insert an item to the queue. 
+
+  */
+  template <typename O>
+  bool try_push(O&& item);
+  
+  /**
+  @brief tries to insert an item to the queue or invoke the callable if fails
+
+  @tparam O data type 
+  @tparam C callable type
+  @param item the item to perfect-forward to the queue
+  @param on_full callable to invoke when the queue is faull (insertion fails)
+  
+  Only the owner thread can insert an item to the queue. 
+
+  */
+  template <typename O, typename C>
+  void push(O&& item, C&& on_full);
+  
+  /**
+  @brief pops out an item from the queue
+
+  Only the owner thread can pop out an item from the queue. 
+  The return can be a @std_nullopt if this operation failed (empty queue).
+  */
+  T pop();
+  
+  /**
+  @brief steals an item from the queue
+
+  Any threads can try to steal an item from the queue.
+  The return can be a @std_nullopt if this operation failed (not necessary empty).
+  */
+  T steal();
+};
+
+// Function: empty
+template <typename T, size_t LogSize>
+bool BoundedTaskQueue<T, LogSize>::empty() const noexcept {
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_relaxed);
+  return b <= t;
+}
+
+// Function: size
+template <typename T, size_t LogSize>
+size_t BoundedTaskQueue<T, LogSize>::size() const noexcept {
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_relaxed);
+  return static_cast<size_t>(b >= t ? b - t : 0);
+}
+
+// Function: try_push
+template <typename T, size_t LogSize>
+template <typename O>
+bool BoundedTaskQueue<T, LogSize>::try_push(O&& o) {
+
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_acquire);
+
+  // queue is full
+  if TF_UNLIKELY((b - t) >= BufferSize - 1) {
+    return false;
+  }
+  
+  _buffer[b & BufferMask].store(std::forward<O>(o), std::memory_order_relaxed);
+
+  std::atomic_thread_fence(std::memory_order_release);
+  
+  // original paper uses relaxed here but tsa complains
+  _bottom.store(b + 1, std::memory_order_release);
+
+  return true;
+}
+
+// Function: push
+template <typename T, size_t LogSize>
+template <typename O, typename C>
+void BoundedTaskQueue<T, LogSize>::push(O&& o, C&& on_full) {
+
+  int64_t b = _bottom.load(std::memory_order_relaxed);
+  int64_t t = _top.load(std::memory_order_acquire);
+
+  // queue is full
+  if TF_UNLIKELY((b - t) >= BufferSize - 1) {
+    on_full();
+    return;
+  }
+  
+  _buffer[b & BufferMask].store(std::forward<O>(o), std::memory_order_relaxed);
+
+  std::atomic_thread_fence(std::memory_order_release);
+  
+  // original paper uses relaxed here but tsa complains
+  _bottom.store(b + 1, std::memory_order_release);
+}
+
+// Function: pop
+template <typename T, size_t LogSize>
+T BoundedTaskQueue<T, LogSize>::pop() {
+
+  int64_t b = _bottom.load(std::memory_order_relaxed) - 1;
+  _bottom.store(b, std::memory_order_relaxed);
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+  int64_t t = _top.load(std::memory_order_relaxed);
+
+  T item {nullptr};
+
+  if(t <= b) {
+    item = _buffer[b & BufferMask].load(std::memory_order_relaxed);
+    if(t == b) {
+      // the last item just got stolen
+      if(!_top.compare_exchange_strong(t, t+1, 
+                                       std::memory_order_seq_cst, 
+                                       std::memory_order_relaxed)) {
+        item = nullptr;
+      }
+      _bottom.store(b + 1, std::memory_order_relaxed);
+    }
+  }
+  else {
+    _bottom.store(b + 1, std::memory_order_relaxed);
+  }
+
+  return item;
+}
+
+// Function: steal
+template <typename T, size_t LogSize>
+T BoundedTaskQueue<T, LogSize>::steal() {
+  int64_t t = _top.load(std::memory_order_acquire);
+  std::atomic_thread_fence(std::memory_order_seq_cst);
+  int64_t b = _bottom.load(std::memory_order_acquire);
+  
+  T item{nullptr};
+
+  if(t < b) {
+    item = _buffer[t & BufferMask].load(std::memory_order_relaxed);
+    if(!_top.compare_exchange_strong(t, t+1,
+                                     std::memory_order_seq_cst,
+                                     std::memory_order_relaxed)) {
+      return nullptr;
+    }
+  }
+
+  return item;
+}
+
+// Function: capacity
+template <typename T, size_t LogSize>
+constexpr size_t BoundedTaskQueue<T, LogSize>::capacity() const {
+  return static_cast<size_t>(BufferSize - 1);
+}
+
+
+
+//-----------------------------------------------------------------------------
+
+//template <typename T>
+//class UnboundedTaskQueue2 {
+//  
+//  static_assert(std::is_pointer_v<T>, "T must be a pointer type");
+//
+//  struct Array {
+//
+//    int64_t C;
+//    int64_t M;
+//    std::atomic<T>* S;
+//
+//    explicit Array(int64_t c) :
+//      C {c},
+//      M {c-1},
+//      S {new std::atomic<T>[static_cast<size_t>(C)]} {
+//    }
+//
+//    ~Array() {
+//      delete [] S;
+//    }
+//
+//    int64_t capacity() const noexcept {
+//      return C;
+//    }
+//
+//    void push(int64_t i, T o) noexcept {
+//      S[i & M].store(o, std::memory_order_relaxed);
+//    }
+//
+//    T pop(int64_t i) noexcept {
+//      return S[i & M].load(std::memory_order_relaxed);
+//    }
+//
+//    Array* resize(int64_t b, int64_t t) {
+//      Array* ptr = new Array {2*C};
+//      for(int64_t i=t; i!=b; ++i) {
+//        ptr->push(i, pop(i));
+//      }
+//      return ptr;
+//    }
+//
+//  };
+//
+//  // Doubling the alignment by 2 seems to generate the most
+//  // decent performance.
+//  alignas(2*TF_CACHELINE_SIZE) std::atomic<int64_t> _top;
+//  alignas(2*TF_CACHELINE_SIZE) std::atomic<int64_t> _bottom;
+//  std::atomic<Array*> _array;
+//  std::vector<Array*> _garbage;
+//
+//  static constexpr int64_t BOTTOM_LOCK = std::numeric_limits<int64_t>::min();
+//  static constexpr int64_t BOTTOM_MASK = std::numeric_limits<int64_t>::max();
+//
+//  public:
+//
+//  /**
+//  @brief constructs the queue with the given size in the base-2 logarithm
+//
+//  @param LogSize the base-2 logarithm of the queue size
+//  */
+//  explicit UnboundedTaskQueue2(int64_t LogSize = TF_DEFAULT_UNBOUNDED_TASK_QUEUE_LOG_SIZE);
+//
+//  /**
+//  @brief destructs the queue
+//  */
+//  ~UnboundedTaskQueue2();
+//
+//  /**
+//  @brief queries if the queue is empty at the time of this call
+//  */
+//  bool empty() const noexcept;
+//
+//  /**
+//  @brief queries the number of items at the time of this call
+//  */
+//  size_t size() const noexcept;
+//
+//  /**
+//  @brief queries the capacity of the queue
+//  */
+//  int64_t capacity() const noexcept;
+//  
+//  /**
+//  @brief inserts an item to the queue
+//
+//  @param item the item to push to the queue
+//  
+//  Only the owner thread can insert an item to the queue.
+//  The operation can trigger the queue to resize its capacity
+//  if more space is required.
+//  */
+//  void push(T item);
+//
+//  /**
+//  @brief steals an item from the queue
+//
+//  Any threads can try to steal an item from the queue.
+//  The return can be a @c nullptr if this operation failed (not necessary empty).
+//  */
+//  T steal();
+//
+//  private:
+//
+//  Array* resize_array(Array* a, int64_t b, int64_t t);
+//};
+//
+//// Constructor
+//template <typename T>
+//UnboundedTaskQueue2<T>::UnboundedTaskQueue2(int64_t LogSize) {
+//  _top.store(0, std::memory_order_relaxed);
+//  _bottom.store(0, std::memory_order_relaxed);
+//  _array.store(new Array{(int64_t{1} << LogSize)}, std::memory_order_relaxed);
+//  _garbage.reserve(32);
+//}
+//
+//// Destructor
+//template <typename T>
+//UnboundedTaskQueue2<T>::~UnboundedTaskQueue2() {
+//  for(auto a : _garbage) {
+//    delete a;
+//  }
+//  delete _array.load();
+//}
+//
+//// Function: empty
+//template <typename T>
+//bool UnboundedTaskQueue2<T>::empty() const noexcept {
+//  int64_t b = _bottom.load(std::memory_order_relaxed) & BOTTOM_MASK;
+//  int64_t t = _top.load(std::memory_order_relaxed);
+//  return (b <= t);
+//}
+//
+//// Function: size
+//template <typename T>
+//size_t UnboundedTaskQueue2<T>::size() const noexcept {
+//  int64_t b = _bottom.load(std::memory_order_relaxed) & BOTTOM_MASK;
+//  int64_t t = _top.load(std::memory_order_relaxed);
+//  return static_cast<size_t>(b >= t ? b - t : 0);
+//}
+//
+//// Function: push
+//template <typename T>
+//void UnboundedTaskQueue2<T>::push(T o) {
+//  
+//  // spin until getting an exclusive access to b
+//  int64_t b = _bottom.load(std::memory_order_acquire) & BOTTOM_MASK;
+//  while(!_bottom.compare_exchange_weak(b, b | BOTTOM_LOCK, std::memory_order_acquire,
+//                                                           std::memory_order_relaxed)) {
+//    b = b & BOTTOM_MASK;
+//  }
+//
+//  // critical region
+//  int64_t t = _top.load(std::memory_order_acquire);
+//  Array* a = _array.load(std::memory_order_relaxed);
+//
+//  // queue is full
+//  if TF_UNLIKELY(a->capacity() - 1 < (b - t)) {
+//    a = resize_array(a, b, t);
+//  }
+//
+//  a->push(b, o);
+//  std::atomic_thread_fence(std::memory_order_release);
+//
+//  // original paper uses relaxed here but tsa complains
+//  _bottom.store(b + 1, std::memory_order_release);
+//}
+//
+//// Function: steal
+//template <typename T>
+//T UnboundedTaskQueue2<T>::steal() {
+//  
+//  int64_t t = _top.load(std::memory_order_acquire);
+//  std::atomic_thread_fence(std::memory_order_seq_cst);
+//  int64_t b = _bottom.load(std::memory_order_acquire) & BOTTOM_MASK;
+//
+//  T item {nullptr};
+//
+//  if(t < b) {
+//    Array* a = _array.load(std::memory_order_consume);
+//    item = a->pop(t);
+//    if(!_top.compare_exchange_strong(t, t+1,
+//                                     std::memory_order_seq_cst,
+//                                     std::memory_order_relaxed)) {
+//      return nullptr;
+//    }
+//  }
+//
+//  return item;
+//}
+//
+//// Function: capacity
+//template <typename T>
+//int64_t UnboundedTaskQueue2<T>::capacity() const noexcept {
+//  return _array.load(std::memory_order_relaxed)->capacity();
+//}
+//
+//template <typename T>
+//typename UnboundedTaskQueue2<T>::Array*
+//UnboundedTaskQueue2<T>::resize_array(Array* a, int64_t b, int64_t t) {
+//
+//  Array* tmp = a->resize(b, t);
+//  _garbage.push_back(a);
+//  std::swap(a, tmp);
+//  _array.store(a, std::memory_order_release);
+//  // Note: the original paper using relaxed causes t-san to complain
+//  //_array.store(a, std::memory_order_relaxed);
+//  return a;
+//}
+
+}  // end of namespace tf -----------------------------------------------------
+
+
diff --git a/bundled/taskflow-3.10.0/taskflow/core/worker.hpp b/bundled/taskflow-3.10.0/taskflow/core/worker.hpp
new file mode 100644 (file)
index 0000000..7b13a64
--- /dev/null
@@ -0,0 +1,275 @@
+#pragma once
+
+#include "declarations.hpp"
+#include "tsq.hpp"
+#include "atomic_notifier.hpp"
+#include "nonblocking_notifier.hpp"
+
+
+/**
+@file worker.hpp
+@brief worker include file
+*/
+
+namespace tf {
+
+// ----------------------------------------------------------------------------
+// Default Notifier
+// ----------------------------------------------------------------------------
+
+/**
+@private
+*/
+#ifdef TF_ENABLE_ATOMIC_NOTIFIER
+  using DefaultNotifier = AtomicNotifierV2;
+#else
+  //using DefaultNotifier = AtomicNotifierV2;
+  using DefaultNotifier = NonblockingNotifierV2;
+#endif
+
+// ----------------------------------------------------------------------------
+// Class Definition: Worker
+// ----------------------------------------------------------------------------
+
+/**
+@class Worker
+
+@brief class to create a worker in an executor
+
+The class is primarily used by the executor to perform work-stealing algorithm.
+Users can access a worker object and alter its property
+(e.g., changing the thread affinity in a POSIX-like system)
+using tf::WorkerInterface.
+*/
+class Worker {
+
+  friend class Executor;
+  friend class Runtime;
+  friend class WorkerView;
+
+  public:
+
+    /**
+    @brief queries the worker id associated with its parent executor
+
+    A worker id is a unsigned integer in the range <tt>[0, N)</tt>,
+    where @c N is the number of workers spawned at the construction
+    time of the executor.
+    */
+    inline size_t id() const { return _id; }
+
+    /**
+    @brief queries the size of the queue (i.e., number of enqueued tasks to
+           run) associated with the worker
+    */
+    inline size_t queue_size() const { return _wsq.size(); }
+    
+    /**
+    @brief queries the current capacity of the queue
+    */
+    inline size_t queue_capacity() const { return static_cast<size_t>(_wsq.capacity()); }
+    
+    /**
+    @brief acquires the associated executor
+    */
+    inline Executor* executor() { return _executor; }
+
+    /**
+    @brief acquires the associated thread
+    */
+    std::thread& thread() { return _thread; }
+
+  private:
+
+    size_t _id;
+    size_t _vtm;
+    Executor* _executor {nullptr};
+    DefaultNotifier::Waiter* _waiter;
+    std::default_random_engine _rdgen;
+    std::uniform_int_distribution<size_t> _rdvtm;
+    std::thread _thread;
+    BoundedTaskQueue<Node*> _wsq;
+};
+
+
+// ----------------------------------------------------------------------------
+// Per-thread
+// ----------------------------------------------------------------------------
+
+namespace pt {
+
+/**
+@private
+*/
+inline thread_local Worker* this_worker {nullptr};
+
+}
+
+// ----------------------------------------------------------------------------
+// Class Definition: WorkerView
+// ----------------------------------------------------------------------------
+
+/**
+@class WorkerView
+
+@brief class to create an immutable view of a worker 
+
+An executor keeps a set of internal worker threads to run tasks.
+A worker view provides users an immutable interface to observe
+when a worker runs a task, and the view object is only accessible
+from an observer derived from tf::ObserverInterface.
+*/
+class WorkerView {
+
+  friend class Executor;
+
+  public:
+
+    /**
+    @brief queries the worker id associated with its parent executor
+
+    A worker id is a unsigned integer in the range <tt>[0, N)</tt>,
+    where @c N is the number of workers spawned at the construction
+    time of the executor.
+    */
+    size_t id() const;
+
+    /**
+    @brief queries the size of the queue (i.e., number of pending tasks to
+           run) associated with the worker
+    */
+    size_t queue_size() const;
+
+    /**
+    @brief queries the current capacity of the queue
+    */
+    size_t queue_capacity() const;
+
+  private:
+
+    WorkerView(const Worker&);
+    WorkerView(const WorkerView&) = default;
+
+    const Worker& _worker;
+
+};
+
+// Constructor
+inline WorkerView::WorkerView(const Worker& w) : _worker{w} {
+}
+
+// function: id
+inline size_t WorkerView::id() const {
+  return _worker._id;
+}
+
+// Function: queue_size
+inline size_t WorkerView::queue_size() const {
+  return _worker._wsq.size();
+}
+
+// Function: queue_capacity
+inline size_t WorkerView::queue_capacity() const {
+  return static_cast<size_t>(_worker._wsq.capacity());
+}
+
+// ----------------------------------------------------------------------------
+// Class Definition: WorkerInterface
+// ----------------------------------------------------------------------------
+
+/**
+@class WorkerInterface
+
+@brief class to configure worker behavior in an executor
+
+The tf::WorkerInterface class allows users to customize worker properties when creating an executor. 
+Examples include binding workers to specific CPU cores or 
+invoking custom methods before and after a worker enters or leaves the work-stealing loop.
+When you create an executor, it spawns a set of workers to execute tasks
+with the following logic:
+
+@code{.cpp}
+for(size_t n=0; n<num_workers; n++) {
+  create_thread([](Worker& worker)
+
+    // pre-processing executor-specific worker information
+    // ...
+
+    // enter the scheduling loop
+    // Here, WorkerInterface::scheduler_prologue is invoked, if any
+    worker_interface->scheduler_prologue(worker);
+    
+    try {
+      while(1) {
+        perform_work_stealing_algorithm();
+        if(stop) {
+          break;
+        }
+      }
+    } catch(...) {
+      exception_ptr = std::current_exception();
+    }
+
+    // leaves the scheduling loop and joins this worker thread
+    // Here, WorkerInterface::scheduler_epilogue is invoked, if any
+    worker_interface->scheduler_epilogue(worker, exception_ptr);
+  );
+}
+@endcode
+
+@attention
+tf::WorkerInterface::scheduler_prologue and tf::WorkerInterface::scheduler_eiplogue 
+are invoked by each worker simultaneously.
+
+*/
+class WorkerInterface {
+
+  public:
+
+  /**
+  @brief default destructor
+  */
+  virtual ~WorkerInterface() = default;
+
+  /**
+  @brief method to call before a worker enters the scheduling loop
+  @param worker a reference to the worker
+
+  The method is called by the constructor of an executor.
+  */
+  virtual void scheduler_prologue(Worker& worker) = 0;
+
+  /**
+  @brief method to call after a worker leaves the scheduling loop
+  @param worker a reference to the worker
+  @param ptr an pointer to the exception thrown by the scheduling loop
+
+  The method is called by the constructor of an executor.
+  */
+  virtual void scheduler_epilogue(Worker& worker, std::exception_ptr ptr) = 0;
+
+};
+
+/**
+@brief helper function to create an instance derived from tf::WorkerInterface
+
+@tparam T type derived from tf::WorkerInterface
+@tparam ArgsT argument types to construct @c T
+
+@param args arguments to forward to the constructor of @c T
+*/
+template <typename T, typename... ArgsT>
+std::unique_ptr<T> make_worker_interface(ArgsT&&... args) {
+  static_assert(
+    std::is_base_of_v<WorkerInterface, T>,
+    "T must be derived from WorkerInterface"
+  );
+  return std::make_unique<T>(std::forward<ArgsT>(args)...);
+}
+
+
+                                                                                 
+                                                                                 
+}  // end of namespact tf ------------------------------------------------------  
+
+
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/cuda/algorithm/sort.hpp
rename to bundled/taskflow-3.10.0/taskflow/cuda/algorithm/sort.hpp
index 3cc01d5ae344eeea335f19224e80bfaf25de1afb..0e563f414f931b3a89d21c0a0360a434226e8007 100644 (file)
@@ -226,7 +226,7 @@ void cuda_merge_sort_partitions(
   unsigned coop, unsigned spacing, C comp, unsigned* buf
 ) {
 
-  // bufer size is num_partitions + 1
+  // buffer size is num_partitions + 1
   unsigned num_partitions = (count + spacing - 1) / spacing + 1;
 
   const unsigned nt = 128;
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/cuda/cuda_capturer.hpp
rename to bundled/taskflow-3.10.0/taskflow/cuda/cuda_capturer.hpp
index 3b5daee9de62657b99f28e8828b5d62658cc148d..c73061cd6c7ee4e72780e5123736a0c6f19d11dc 100644 (file)
@@ -79,7 +79,7 @@ class cudaFlowCapturer {
   public:
 
     /**
-    @brief constrcts a standalone cudaFlowCapturer
+    @brief constructs a standalone cudaFlowCapturer
 
     A standalone %cudaFlow capturer does not go through any taskflow and
     can be run by the caller thread using tf::cudaFlowCapturer::run.
@@ -232,7 +232,7 @@ class cudaFlowCapturer {
     /**
     @brief initializes or sets GPU memory to the given value byte by byte
 
-    @param ptr pointer to GPU mempry
+    @param ptr pointer to GPU memory
     @param v value to set for each byte of the specified memory
     @param n size in bytes to set
 
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/cuda/cuda_device.hpp
rename to bundled/taskflow-3.10.0/taskflow/cuda/cuda_device.hpp
index 016b2a6f642939c045a619653b58bef052e26887..058bef14eaf5fcb39aa61fb708bd5a0c991663fe 100644 (file)
@@ -76,7 +76,7 @@ inline void cuda_dump_device_property(std::ostream& os, const cudaDeviceProp& p)
   }
   os << '\n';
 
-  os << "Maximum dimenstion of grid:    ";
+  os << "Maximum dimension of grid:    ";
   for (int i = 0; i < 3; ++i) {
     if(i) os << 'x';
     os << p.maxGridSize[i];;
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/cuda/cuda_graph.hpp
rename to bundled/taskflow-3.10.0/taskflow/cuda/cuda_graph.hpp
index a326aedeaec7e205e4e7219b506162b52e6cb2bb..3cfee37d8afaec3c1871f497303a5838d2402c0d 100644 (file)
@@ -421,7 +421,7 @@ class cudaGraphExec :
   cudaGraphExec() = default;
   
   /**
-  @brief instantiates the exexutable from the given CUDA graph
+  @brief instantiates the executable from the given CUDA graph
   */
   void instantiate(cudaGraph_t graph) {
     cudaGraphExecDeleter {} (object);
@@ -432,7 +432,7 @@ class cudaGraphExec :
   }
   
   /**
-  @brief updates the exexutable from the given CUDA graph
+  @brief updates the executable from the given CUDA graph
   */
   cudaGraphExecUpdateResult update(cudaGraph_t graph) {
     cudaGraphNode_t error_node;
@@ -442,7 +442,7 @@ class cudaGraphExec :
   }
   
   /**
-  @brief launchs the executable graph via the given stream
+  @brief launches the executable graph via the given stream
   */
   void launch(cudaStream_t stream) {
     TF_CHECK_CUDA(
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/cuda/cuda_memory.hpp
rename to bundled/taskflow-3.10.0/taskflow/cuda/cuda_memory.hpp
index 44648683e79edc373a631b3bc0bac9ffc2a4467a..032dca1d03f8d164174bc33e2716379a6decf065 100644 (file)
@@ -142,7 +142,7 @@ inline void cuda_memcpy_async(
 @brief initializes or sets GPU memory to the given value byte by byte
 
 @param stream stream identifier
-@param devPtr pointer to GPU mempry
+@param devPtr pointer to GPU memory
 @param value value to set for each byte of the specified memory
 @param count size in bytes to set
 
@@ -529,7 +529,7 @@ class cudaDeviceAllocator {
   A call to member allocate with the value returned by this function 
   can still fail to allocate the requested storage.
   
-  @return the nubmer of elements that might be allcoated as maximum 
+  @return the number of elements that might be allocated as maximum 
           by a call to member allocate
   */
   size_type max_size() const noexcept { return size_type {-1}; }
@@ -725,7 +725,7 @@ class cudaUSMAllocator {
   A call to member allocate with the value returned by this function 
   can still fail to allocate the requested storage.
   
-  @return the nubmer of elements that might be allcoated as maximum 
+  @return the number of elements that might be allocated as maximum 
           by a call to member allocate
   */
   size_type max_size() const noexcept { return size_type {-1}; }
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/cuda/cuda_object.hpp
rename to bundled/taskflow-3.10.0/taskflow/cuda/cuda_object.hpp
index e30d3a52da7180f9fbc8934a74f23c4c0289a85c..9090c2c70d708490b099675e9e6b002d06db4a28 100644 (file)
@@ -15,7 +15,7 @@ A CUDA device object has a lifetime associated with a device,
 for example, @c cudaStream_t, @c cublasHandle_t, etc.
 Creating a device object is typically expensive (e.g., 10-200 ms)
 and destroying it may trigger implicit device synchronization.
-For applications tha intensively make use of device objects,
+For applications that intensively make use of device objects,
 it is desirable to reuse them as much as possible.
 
 There exists an one-to-one relationship between CUDA devices in CUDA Runtime API
diff --git a/bundled/taskflow-3.10.0/taskflow/taskflow.hpp b/bundled/taskflow-3.10.0/taskflow/taskflow.hpp
new file mode 100644 (file)
index 0000000..6f669e2
--- /dev/null
@@ -0,0 +1,104 @@
+#pragma once
+
+// Feature macros for fine-tuning the performance of Taskflow at compile time
+// 
+// Disabled features by default:
+// + TF_ENABLE_TASK_POOL       : enable task pool optimization
+// + TF_ENABLE_ATOMIC_NOTIFIER : enable atomic notifier (required C++20)
+//
+
+#include "core/executor.hpp"
+#include "core/runtime.hpp"
+#include "core/async.hpp"
+#include "algorithm/algorithm.hpp"
+
+/**
+@dir taskflow
+@brief root taskflow include dir
+*/
+
+/**
+@dir taskflow/core
+@brief taskflow core include dir
+*/
+
+/**
+@dir taskflow/algorithm
+@brief taskflow algorithms include dir
+*/
+
+/**
+@dir taskflow/cuda
+@brief taskflow CUDA include dir
+*/
+
+/**
+@file taskflow/taskflow.hpp
+@brief main taskflow include file
+*/
+
+
+
+/**
+@def TF_VERSION 
+
+@brief version of the %Taskflow (currently 3.10.0)
+
+The version system is made of a major version number, a minor version number,
+and a patch number:
+  + TF_VERSION % 100 is the patch level
+  + TF_VERSION / 100 % 1000 is the minor version
+  + TF_VERSION / 100000 is the major version
+*/
+#define TF_VERSION 301000
+
+/**
+@def TF_MAJOR_VERSION
+
+@brief major version of %Taskflow, which is equal to `TF_VERSION/100000`
+*/
+#define TF_MAJOR_VERSION TF_VERSION/100000
+
+/**
+@def TF_MINOR_VERSION
+
+@brief minor version of %Taskflow, which is equal to `TF_VERSION / 100 % 1000`
+*/
+#define TF_MINOR_VERSION TF_VERSION/100%1000
+
+/**
+@def TF_PATCH_VERSION
+
+@brief patch version of %Taskflow, which is equal to `TF_VERSION % 100`
+*/
+#define TF_PATCH_VERSION TF_VERSION%100
+
+
+
+/**
+@brief taskflow namespace
+*/
+namespace tf {
+
+/**
+@private
+*/
+namespace detail { }
+
+
+/**
+@brief queries the version information in a string format @c major.minor.patch
+
+Release notes are available here: https://taskflow.github.io/taskflow/Releases.html
+*/
+constexpr const char* version() {
+  return "3.10.0";
+}
+
+
+}  // end of namespace tf -----------------------------------------------------
+
+
+
+
+
diff --git a/bundled/taskflow-3.10.0/taskflow/utility/iterator.hpp b/bundled/taskflow-3.10.0/taskflow/utility/iterator.hpp
new file mode 100644 (file)
index 0000000..b861a20
--- /dev/null
@@ -0,0 +1,208 @@
+#pragma once
+
+#include <cstddef>
+#include <type_traits>
+
+namespace tf {
+
+/**
+ * @brief checks if the given index range is invalid
+ *
+ * @tparam B type of the beginning index
+ * @tparam E type of the ending index
+ * @tparam S type of the step size
+ *
+ * @param beg starting index of the range
+ * @param end ending index of the range
+ * @param step step size to traverse the range
+ *
+ * @return returns @c true if the range is invalid; @c false otherwise.
+ *
+ * A range is considered invalid under the following conditions:
+ *  + The step is zero and the begin and end values are not equal.
+ *  + A positive range (begin < end) with a non-positive step.
+ *  + A negative range (begin > end) with a non-negative step.
+ */
+template <typename B, typename E, typename S>
+constexpr std::enable_if_t<std::is_integral_v<std::decay_t<B>> && 
+                           std::is_integral_v<std::decay_t<E>> && 
+                           std::is_integral_v<std::decay_t<S>>, bool>
+is_index_range_invalid(B beg, E end, S step) {
+  return ((step == 0 && beg != end) ||
+          (beg < end && step <=  0) ||  // positive range
+          (beg > end && step >=  0));   // negative range
+}
+
+/**
+ * @brief calculates the number of iterations in the given index range
+ *
+ * @tparam B type of the beginning index
+ * @tparam E type of the ending index
+ * @tparam S type of the step size
+ *
+ * @param beg starting index of the range
+ * @param end ending index of the range
+ * @param step step size to traverse the range
+ *
+ * @return returns the number of required iterations to traverse the range
+ *
+ * The distance of a range represents the number of required iterations to traverse the range
+ * from the beginning index to the ending index (exclusive) with the given step size.
+ * 
+ * Example 1:
+ * @code{.cpp}
+ * // Range: 0 to 10 with step size 2
+ * size_t dist = distance(0, 10, 2);  // Returns 5, the sequence is [0, 2, 4, 6, 8]
+ * @endcode
+ *
+ * Example 2:
+ * @code{.cpp}
+ * // Range: 10 to 0 with step size -2
+ * size_t dist = distance(10, 0, -2);  // Returns 5, the sequence is [10, 8, 6, 4, 2]
+ * @endcode
+ *
+ * Example 3:
+ * @code{.cpp}
+ * // Range: 5 to 20 with step size 5
+ * size_t dist = distance(5, 20, 5);  // Returns 3, the sequence is [5, 10, 15]
+ * @endcode
+ *
+ * @attention
+ * It is user's responsibility to ensure the given index range is valid.
+ */
+template <typename B, typename E, typename S>
+constexpr std::enable_if_t<std::is_integral_v<std::decay_t<B>> && 
+                           std::is_integral_v<std::decay_t<E>> && 
+                           std::is_integral_v<std::decay_t<S>>, size_t>
+distance(B beg, E end, S step) {
+  return (end - beg + step + (step > 0 ? -1 : 1)) / step;
+}
+
+/**
+ * @class IndexRange
+ *
+ * @brief class to create an index range of integral indices with a step size
+ *
+ * This class provides functionality for managing a range of indices, where the range 
+ * is defined by a starting index, an ending index, and a step size. The indices must 
+ * be of an integral type.
+ * For example, the range [0, 10) with a step size 2 represents the five elements,
+ * 0, 2, 4, 6, and 8.
+ *
+ * @tparam T the integral type of the indices
+ *
+ * @attention
+ * It is user's responsibility to ensure the given range is valid.
+ */
+template <typename T>
+class IndexRange {
+
+  static_assert(std::is_integral_v<T>, "index type must be integral");
+
+public:
+
+  /**
+  @brief alias for the index type used in the range
+  */
+  using index_type = T;
+
+  /**
+  @brief constructs an index range object without any initialization
+  */
+  IndexRange() = default;
+
+  /**
+   * @brief constructs an IndexRange object
+   * @param beg starting index of the range
+   * @param end ending index of the range (exclusive)
+   * @param step_size step size between consecutive indices in the range
+   */
+  explicit IndexRange(T beg, T end, T step_size)
+    : _beg{beg}, _end{end}, _step_size{step_size} {}
+
+  /**
+   * @brief queries the starting index of the range
+   */
+  T begin() const { return _beg; }
+
+  /**
+   * @brief queries the ending index of the range
+   */
+  T end() const { return _end; }
+
+  /**
+   * @brief queries the step size of the range
+   */
+  T step_size() const { return _step_size; }
+
+  /**
+   * @brief updates the range with the new starting index, ending index, and step size
+   */
+  IndexRange<T>& reset(T begin, T end, T step_size) {
+    _beg = begin;
+    _end = end;
+    _step_size = step_size;
+    return *this;
+  }
+
+  /**
+   * @brief updates the starting index of the range
+   */
+  IndexRange<T>& begin(T new_begin) { _beg = new_begin; return *this; }
+
+  /**
+   * @brief updates the ending index of the range
+   */
+  IndexRange<T>& end(T new_end) { _end = new_end; return *this; }
+
+  /**
+   * @brief updates the step size of the range
+   */
+  IndexRange<T>& step_size(T new_step_size) { _step_size = new_step_size; return *this; }
+
+  /**
+   * @brief queries the number of elements in the range
+   *
+   * The number of elements is equivalent to the number of iterations in the range.
+   * For instance, the range [0, 10) with step size of 2 will iterate five elements,
+   * 0, 2, 4, 6, and 8.
+   */
+  size_t size() const { return distance(_beg, _end, _step_size); }
+
+  /**
+   * @brief returns a range from the given discrete domain
+   * @param part_beg starting index of the discrete domain
+   * @param part_end ending index of the discrete domain
+   * @return a new IndexRange object representing the given discrete domain
+   * 
+   * The discrete domain of a range refers to a counter-based sequence indexed from 0
+   * to @c N, where @c N is the size (i.e., number of iterated elements) of the range. 
+   * For example, a discrete domain of the range [0, 10) with a step size of 2 corresponds 
+   * to the sequence 0, 1, 2, 3, and 4, which map to the range elements 0, 2, 4, 6, and 8.
+   *
+   * For a partitioned domain [@c part_beg, @c part_end), this function returns
+   * the corresponding range. For instance, the partitioned domain [2, 5) for the
+   * above example returns the range [4, 10) with the same step size of 2.
+   *
+   * @attention
+   * Users must ensure the specified domain is valid with respect to the range.
+   */
+  IndexRange discrete_domain(size_t part_beg, size_t part_end) const {
+    return IndexRange(
+      static_cast<T>(part_beg) * _step_size + _beg,
+      static_cast<T>(part_end) * _step_size + _beg,
+      _step_size
+    );
+  }
+
+  private:
+
+  T _beg;
+  T _end;
+  T _step_size;
+
+};
+
+  
+
+}  // end of namespace tf -----------------------------------------------------
diff --git a/bundled/taskflow-3.10.0/taskflow/utility/latch.hpp b/bundled/taskflow-3.10.0/taskflow/utility/latch.hpp
new file mode 100644 (file)
index 0000000..af292e7
--- /dev/null
@@ -0,0 +1,77 @@
+#pragma once
+
+
+// use tf::Latch
+#include <condition_variable>
+#include <limits>
+#include <mutex>
+
+namespace tf {
+
+class Latch {
+
+private:
+
+  std::ptrdiff_t _counter;
+  mutable std::condition_variable _cv;
+  mutable std::mutex _mutex;
+
+public:
+
+  static constexpr ptrdiff_t (max)() noexcept
+  {
+    return (std::numeric_limits<ptrdiff_t>::max)();
+  }
+
+  explicit Latch(std::ptrdiff_t expected)
+    : _counter(expected)
+  {
+    assert(0 <= expected && expected < (max)());
+  }
+
+  ~Latch() = default;
+
+  Latch(const Latch&) = delete;
+  Latch& operator=(const Latch&) = delete;
+
+  void count_down(std::ptrdiff_t update = 1)
+  {
+    std::lock_guard<decltype(_mutex)> lk(_mutex);
+    assert(0 <= update && update <= _counter);
+    _counter -= update;
+    if (_counter == 0) {
+      _cv.notify_all();
+    }
+  }
+
+  bool try_wait() const noexcept
+  {
+    std::lock_guard<decltype(_mutex)> lk(_mutex);
+    // no spurious failure
+    return (_counter == 0);
+  }
+
+  void wait() const
+  {
+    std::unique_lock<decltype(_mutex)> lk(_mutex);
+    while (_counter != 0) {
+      _cv.wait(lk);
+    }
+  }
+
+  void arrive_and_wait(std::ptrdiff_t update = 1)
+  {
+    std::unique_lock<decltype(_mutex)> lk(_mutex);
+    // equivalent to { count_down(update); wait(); }
+    assert(0 <= update && update <= _counter);
+    _counter -= update;
+    if (_counter == 0) {
+      _cv.notify_all();
+    }
+    while (_counter != 0) {
+      _cv.wait(lk);
+    }
+  }
+};
+
+} // namespace tf -------------------------------------------------------------
similarity index 52%
rename from bundled/taskflow-3.7.0/taskflow/utility/macros.hpp
rename to bundled/taskflow-3.10.0/taskflow/utility/macros.hpp
index 988e0e1dcab2c52e65b70485a29043c650a01494..b22b119248ba89da3074df30a662717543fd7a4b 100644 (file)
@@ -1,9 +1,20 @@
 #pragma once
 
+// ============================================================================
+// C++ Versions
+// ============================================================================
+#define TF_CPP98 199711L
+#define TF_CPP11 201103L
+#define TF_CPP14 201402L
+#define TF_CPP17 201703L
+#define TF_CPP20 202002L
+
+// ============================================================================
+// inline and no-inline
+// ============================================================================
+
 #if defined(_MSC_VER)
   #define TF_FORCE_INLINE __forceinline
-#elif defined(__CUDA__) && defined(__clang__)
-  #define TF_FORCE_INLINE inline
 #elif defined(__GNUC__) && __GNUC__ > 3
   #define TF_FORCE_INLINE __attribute__((__always_inline__)) inline
 #else
 
 #if defined(_MSC_VER)
   #define TF_NO_INLINE __declspec(noinline)
-#elif defined(__CUDA__) && defined(__clang__)
-  #define TF_NO_INLINE
 #elif defined(__GNUC__) && __GNUC__ > 3
   #define TF_NO_INLINE __attribute__((__noinline__))
 #else
   #define TF_NO_INLINE
 #endif
 
+// ============================================================================
+// likely and unlikely
+// ============================================================================
+
+#if defined(__GNUC__)
+  #define TF_LIKELY(x) (__builtin_expect((x), 1))
+  #define TF_UNLIKELY(x) (__builtin_expect((x), 0))
+#else
+  #define TF_LIKELY(x) (x)
+  #define TF_UNLIKELY(x) (x)
+#endif
+
 // ----------------------------------------------------------------------------
 
 #ifdef TF_DISABLE_EXCEPTION_HANDLING
 #endif
 
 // ----------------------------------------------------------------------------    
+
+#define TF_FWD(T, x) std::forward<T>(x)
+
+
+
+
+
+
+
+
+
+
diff --git a/bundled/taskflow-3.10.0/taskflow/utility/math.hpp b/bundled/taskflow-3.10.0/taskflow/utility/math.hpp
new file mode 100644 (file)
index 0000000..15685a4
--- /dev/null
@@ -0,0 +1,322 @@
+#pragma once
+
+#include <atomic>
+
+namespace tf {
+
+/**
+ * @brief rounds the given 64-bit unsigned integer to the nearest power of 2
+ */
+template <typename T, std::enable_if_t<
+  (std::is_unsigned_v<std::decay_t<T>> && sizeof(T) == 8), void
+>* = nullptr>
+constexpr T next_pow2(T x) {
+  if(x == 0) return 1;
+  x--;
+  x |= x >> 1;
+  x |= x >> 2;
+  x |= x >> 4;
+  x |= x >> 8;
+  x |= x >> 16;
+  x |= x >> 32;
+  x++;
+  return x;
+}
+
+/**
+ * @brief rounds the given 32-bit unsigned integer to the nearest power of 2
+ */
+template <typename T, std::enable_if_t<
+  (std::is_unsigned_v<std::decay_t<T>> && sizeof(T) == 4), void
+>* = nullptr>
+constexpr T next_pow2(T y) {
+  if(y == 0) return 1;
+  y--;
+  y |= y >> 1;
+  y |= y >> 2;
+  y |= y >> 4;
+  y |= y >> 8;
+  y |= y >> 16;
+  y++;
+  return y;
+}
+
+/**
+ * @brief checks if the given number is a power of 2
+ *
+ * This function determines if the given integer is a power of 2.
+ *
+ * @tparam T The type of the input. Must be an integral type.
+ * @param x The integer to check.
+ * @return `true` if `x` is a power of 2, otherwise `false`.
+ *
+ * @attention This function is constexpr and can be evaluated at compile time.
+ *
+ */
+template <typename T, std::enable_if_t<
+  std::is_integral_v<std::decay_t<T>>, void>* = nullptr
+>
+constexpr bool is_pow2(const T& x) {
+  return x && (!(x&(x-1)));
+}
+
+/**
+ * @brief Computes the floor of log2 of the given positive integer.
+ *
+ * This function calculates the largest integer `log` such that `2^log <= n`.
+ *
+ * @tparam T The type of the input. Must be an integral type.
+ * @param n The positive integer to compute log2 for. Assumes `n > 0`.
+ * @return The floor of log2 of `n`.
+ *
+ * @attention This function is constexpr and can be evaluated at compile time.
+ *
+ */
+template<typename T>
+constexpr int log2(T n) {
+  int log = 0;
+  while (n >>= 1) {
+    ++log;
+  }
+  return log;
+}
+
+/**
+ * @brief finds the median of three numbers pointed to by iterators using the given comparator
+ *
+ * This function determines the median value of the elements pointed to by
+ * three random-access iterators using the provided comparator.
+ *
+ * @tparam RandItr The type of the random-access iterator.
+ * @tparam C The type of the comparator.
+ * @param l Iterator to the first element.
+ * @param m Iterator to the second element.
+ * @param r Iterator to the third element.
+ * @param cmp The comparator used to compare the dereferenced iterator values.
+ * @return The iterator pointing to the median value among the three elements.
+ *
+ */
+template <typename RandItr, typename C>
+RandItr median_of_three(RandItr l, RandItr m, RandItr r, C cmp) {
+  return cmp(*l, *m) ? (cmp(*m, *r) ? m : (cmp(*l, *r) ? r : l ))
+                     : (cmp(*r, *m) ? m : (cmp(*r, *l) ? r : l ));
+}
+
+/**
+ * @brief finds the pseudo median of a range of items using a spread of nine numbers
+ *
+ * This function computes an approximate median of a range of items by sampling
+ * nine values spread across the range and finding their median. It uses a
+ * combination of the `median_of_three` function to determine the pseudo median.
+ *
+ * @tparam RandItr The type of the random-access iterator.
+ * @tparam C The type of the comparator.
+ * @param beg Iterator to the beginning of the range.
+ * @param end Iterator to the end of the range.
+ * @param cmp The comparator used to compare the dereferenced iterator values.
+ * @return The iterator pointing to the pseudo median of the range.
+ *
+ * @attention The pseudo median is an approximation of the true median and may not
+ *       be the exact middle value of the range.
+ *
+ */
+template <typename RandItr, typename C>
+RandItr pseudo_median_of_nine(RandItr beg, RandItr end, C cmp) {
+  size_t N = std::distance(beg, end);
+  size_t offset = N >> 3;
+  return median_of_three(
+    median_of_three(beg, beg+offset, beg+(offset*2), cmp),
+    median_of_three(beg+(offset*3), beg+(offset*4), beg+(offset*5), cmp),
+    median_of_three(beg+(offset*6), beg+(offset*7), end-1, cmp),
+    cmp
+  );
+}
+
+/**
+ * @brief sorts two elements of dereferenced iterators using the given comparison function
+ *
+ * This function compares two elements pointed to by iterators and swaps them
+ * if they are out of order according to the provided comparator.
+ *
+ * @tparam Iter The type of the iterator.
+ * @tparam Compare The type of the comparator.
+ * @param a Iterator to the first element.
+ * @param b Iterator to the second element.
+ * @param comp The comparator used to compare the dereferenced iterator values.
+ *
+ */
+template<typename Iter, typename Compare>
+void sort2(Iter a, Iter b, Compare comp) {
+  if (comp(*b, *a)) std::iter_swap(a, b);
+}
+
+/**
+ * @brief Sorts three elements of dereferenced iterators using the given comparison function.
+ *
+ * This function sorts three elements pointed to by iterators in ascending order
+ * according to the provided comparator. The sorting is performed using a sequence
+ * of calls to the `sort2` function to ensure the correct order of elements.
+ *
+ * @tparam Iter The type of the iterator.
+ * @tparam Compare The type of the comparator.
+ * @param a Iterator to the first element.
+ * @param b Iterator to the second element.
+ * @param c Iterator to the third element.
+ * @param comp The comparator used to compare the dereferenced iterator values.
+ *
+ */
+template<typename Iter, typename Compare>
+void sort3(Iter a, Iter b, Iter c, Compare comp) {
+  sort2(a, b, comp);
+  sort2(b, c, comp);
+  sort2(a, b, comp);
+}
+
+/**
+ * @brief generates a program-wide unique ID of the given type in a thread-safe manner
+ *
+ * This function provides a globally unique identifier of the specified integral type.
+ * It uses a static `std::atomic` counter to ensure thread safety and increments the
+ * counter in a relaxed memory ordering for efficiency.
+ *
+ * @tparam T The type of the ID to generate. Must be an integral type.
+ * @return A unique ID of type `T`.
+ *
+ * @attention The uniqueness of the ID is guaranteed only within the program's lifetime.
+ * @attention The function does not throw exceptions.
+ *
+ */
+template <typename T, std::enable_if_t<std::is_integral_v<T>, void>* = nullptr>
+T unique_id() {
+  static std::atomic<T> counter{0};
+  return counter.fetch_add(1, std::memory_order_relaxed);
+}
+
+/**
+ * @brief updates an atomic variable with the maximum value
+ *
+ * This function atomically updates the provided atomic variable `v` to hold
+ * the maximum of its current value and `max_v`. The update is performed using
+ * a relaxed memory ordering for efficiency in non-synchronizing contexts.
+ *
+ * @tparam T The type of the atomic variable. Must be trivially copyable and comparable.
+ * @param v The atomic variable to update.
+ * @param max_v The value to compare with the current value of `v`.
+ *
+ * @attention If multiple threads call this function concurrently, the value of `v`
+ *       will be the maximum value seen across all threads.
+ *
+ */
+template <typename T>
+inline void atomic_max(std::atomic<T>& v, const T& max_v) noexcept {
+  T prev = v.load(std::memory_order_relaxed);
+  while(prev < max_v && 
+        !v.compare_exchange_weak(prev, max_v, std::memory_order_relaxed,
+                                              std::memory_order_relaxed)) {
+  }
+}
+
+/**
+ * @brief updates an atomic variable with the minimum value
+ *
+ * This function atomically updates the provided atomic variable `v` to hold
+ * the minimum of its current value and `min_v`. The update is performed using 
+ * a relaxed memory ordering for efficiency in non-synchronizing contexts.
+ *
+ * @tparam T The type of the atomic variable. Must be trivially copyable and comparable.
+ * @param v The atomic variable to update.
+ * @param min_v The value to compare with the current value of `v`.
+ *
+ * @attention If multiple threads call this function concurrently, the value of `v` 
+ *       will be the minimum value seen across all threads.
+ *
+ */
+template <typename T>
+inline void atomic_min(std::atomic<T>& v, const T& min_v) noexcept {
+  T prev = v.load(std::memory_order_relaxed);
+  while(prev > min_v && 
+        !v.compare_exchange_weak(prev, min_v, std::memory_order_relaxed,
+                                              std::memory_order_relaxed)) {
+  }
+}
+
+/**
+ * @brief generates a random seed based on the current system clock
+ *
+ * This function returns a seed value derived from the number of clock ticks
+ * since the epoch as measured by the system clock. The seed can be used
+ * to initialize random number generators.
+ *
+ * @tparam T The type of the returned seed. Must be an integral type.
+ * @return A seed value based on the system clock.
+ *
+ */
+template <typename T>
+inline T seed() noexcept {
+  return std::chrono::system_clock::now().time_since_epoch().count();
+}
+
+//class XorShift64 {
+//
+//  public:
+//  
+//  explicit XorShift64(uint64_t seed) : _state(seed) {}
+//
+//  uint64_t next() {
+//    _state ^= _state >> 12;
+//    _state ^= _state << 25;
+//    _state ^= _state >> 27;
+//    return _state * 0x2545F4914F6CDD1DULL; // Scramble for better randomness
+//  }
+//
+//  size_t random_range(size_t min, size_t max) {
+//    return min + (next() % (max - min + 1));
+//  }
+//
+//  private:
+//
+//  uint64_t _state;
+//};
+
+//inline int generate_random_excluding(int worker_id, int W, XorShift64& rng) {
+//    int random_number = rng.random_range(0, 2 * W - 2); // Range: [0, 2W-2]
+//    return random_number + (random_number >= worker_id); // Skip worker_id
+//}
+//
+//
+//class Xoroshiro128Plus {
+//
+//  public:
+//
+//    explicit Xoroshiro128Plus(uint64_t seed1, uint64_t seed2) : _state{seed1, seed2} {}
+//
+//    uint64_t next() {
+//      uint64_t s0 = _state[0];
+//      uint64_t s1 = _state[1];
+//      uint64_t result = s0 + s1;
+//
+//      s1 ^= s0;
+//      _state[0] = _rotl(s0, 55) ^ s1 ^ (s1 << 14); // Scramble _state
+//      _state[1] = _rotl(s1, 36);
+//
+//      return result;
+//    }
+//
+//    int random_range(int min, int max) {
+//      return min + (next() % (max - min + 1));
+//    }
+//
+//  private:
+//
+//    std::array<uint64_t, 2> _state;
+//
+//    static uint64_t _rotl(uint64_t x, int k) {
+//      return (x << k) | (x >> (64 - k));
+//    }
+//};
+
+
+}  // end of namespace tf -----------------------------------------------------
+
+
+
diff --git a/bundled/taskflow-3.10.0/taskflow/utility/mpmc.hpp b/bundled/taskflow-3.10.0/taskflow/utility/mpmc.hpp
new file mode 100644 (file)
index 0000000..1c74cef
--- /dev/null
@@ -0,0 +1,152 @@
+#pragma once
+
+#include <cassert>
+#include <atomic>
+#include <optional>
+
+namespace tf {
+
+/**
+ * A 'lockless' bounded multi-producer, multi-consumer queue
+ *
+ * Has the caveat that the queue can *appear* empty even if there are
+ * returned items within it as a single thread can block progression
+ * of the queue.
+ */
+template<typename T, size_t LogSize = 10>
+class MPMC {
+
+  constexpr static uint64_t BufferSize = 1ull << LogSize;
+  constexpr static uint64_t BufferMask = (BufferSize - 1);
+  
+  static_assert((BufferSize >= 2) && ((BufferSize & (BufferSize - 1)) == 0));
+
+public:
+
+  /**
+   * Constructs a bounded multi-producer, multi-consumer queue
+   *
+   * Note: Due to the algorithm used, buffer_size must be a power
+   *       of two and must be greater than or equal to two.
+   *
+   * @param buffer_size Number of spaces available in the queue.
+   */
+  explicit MPMC() {
+    for (size_t i = 0; i < _buffer.size(); i++) {
+      _buffer[i].sequence.store(i, std::memory_order_relaxed);
+    }
+    _enqueue_pos.store(0, std::memory_order_relaxed);
+    _dequeue_pos.store(0, std::memory_order_relaxed);
+  }
+
+
+  /**
+   * Enqueues an item into the queue
+   *
+   * @param data Argument to place into the array
+   * @return false if the queue was full (and enqueing failed),
+   *         true otherwise
+   */
+  bool try_enqueue(T data) {
+    Cell *cell;
+    auto pos = _enqueue_pos.load(std::memory_order_relaxed);
+    for (; ;) {
+      cell = &_buffer[pos & BufferMask];
+      auto seq = cell->sequence.load(std::memory_order_acquire);
+      if (seq == pos) {
+        if (_enqueue_pos.compare_exchange_weak(pos, pos + 1,
+                                               std::memory_order_relaxed)) {
+            break;
+        }
+      } else if (seq < pos) {
+          return false;
+      } else {
+          pos = _enqueue_pos.load(std::memory_order_relaxed);
+      }
+    }
+
+    cell->data = data;
+    cell->sequence.store(pos + 1, std::memory_order_release);
+
+    return true;
+  }
+  
+  void enqueue(T data) {
+
+    Cell *cell;
+    auto pos = _enqueue_pos.load(std::memory_order_relaxed);
+
+    for (; ;) {
+      cell = &_buffer[pos & BufferMask];
+      auto seq = cell->sequence.load(std::memory_order_acquire);
+      if (seq == pos) {
+        if (_enqueue_pos.compare_exchange_weak(pos, pos + 1,
+                                               std::memory_order_relaxed)) {
+            break;
+        }
+      }
+      else {
+        pos = _enqueue_pos.load(std::memory_order_relaxed);
+      }
+    }
+
+    cell->data = data;
+    cell->sequence.store(pos + 1, std::memory_order_release);
+  }
+
+  /**
+   * Dequeues an item from the queue
+   *
+   * @param[out] data Reference to place item into
+   * @return false if the queue was empty (and dequeuing failed),
+   *         true if successful
+   */
+  std::optional<T> try_dequeue() {
+    Cell *cell;
+    auto pos = _dequeue_pos.load(std::memory_order_relaxed);
+    for (; ;) {
+      cell = &_buffer[pos & BufferMask];
+      auto seq = cell->sequence.load(std::memory_order_acquire);
+      if (seq == pos + 1) {
+        if (_dequeue_pos.compare_exchange_weak(pos, pos + 1,
+                                               std::memory_order_relaxed)) {
+          break;
+        }
+      } else if (seq < (pos + 1)) {
+        return std::nullopt;
+      } else {
+        pos = _dequeue_pos.load(std::memory_order_relaxed);
+      }
+    }
+
+    T data = cell->data;
+    cell->sequence.store(pos + BufferMask + 1, std::memory_order_release);
+
+    return data;
+  }
+
+  bool empty() const {
+    auto beg = _dequeue_pos.load(std::memory_order_relaxed);
+    auto end = _enqueue_pos.load(std::memory_order_relaxed);
+    return beg >= end;
+  }
+
+  size_t capacity() const {
+    return BufferSize;
+  }
+
+private:
+
+  struct Cell {
+    T data;
+    std::atomic<uint64_t> sequence;
+  };
+
+  //static const size_t cacheline_size = 64;
+
+  alignas(2*TF_CACHELINE_SIZE) std::array<Cell, BufferSize> _buffer;
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<uint64_t> _enqueue_pos;
+  alignas(2*TF_CACHELINE_SIZE) std::atomic<uint64_t> _dequeue_pos;
+};
+
+}  // end of namespace tf -----------------------------------------------------
similarity index 98%
rename from bundled/taskflow-3.7.0/taskflow/utility/object_pool.hpp
rename to bundled/taskflow-3.10.0/taskflow/utility/object_pool.hpp
index 34d60fb8029a0cb6a1434a00fddc65bba602ef72..d9f2254947d24a001f3e844c31ae5f67278cd291 100644 (file)
@@ -32,7 +32,7 @@ namespace tf {
 // Different from the normal memory allocator, object pool allocates
 // only one object at a time.
 //
-// Internall, we use the following variables to maintain blocks and heaps:
+// Internally, we use the following variables to maintain blocks and heaps:
 // X: size in byte of a item slot
 // M: number of items per block
 // F: emptiness threshold
@@ -356,7 +356,7 @@ template <class P, class Q>
 constexpr P* ObjectPool<T, S>::_parent_class_of(
   Q* ptr, const Q P::*member
 ) {
-  return (P*)( (char*)ptr - _offset_in_class(member));
+  return reinterpret_cast<P*>(reinterpret_cast<char*>(ptr) - _offset_in_class(member));
 }
 
 // Function: _parent_class_of
@@ -365,7 +365,7 @@ template <class P, class Q>
 constexpr P* ObjectPool<T, S>::_parent_class_of(
   const Q* ptr, const Q P::*member
 ) const {
-  return (P*)( (char*)ptr - _offset_in_class(member));
+  return reinterpret_cast<P*>(reinterpret_cast<char*>(ptr) - _offset_in_class(member));
 }
 
 // Function: _block_of
@@ -625,10 +625,6 @@ T* ObjectPool<T, S>::animate(ArgsT&&... args) {
       //s = static_cast<Block*>(std::malloc(sizeof(Block)));
       s = new Block();
 
-      if(s == nullptr) {
-        throw std::bad_alloc();
-      }
-
       s->heap = &h;
       s->i = 0;
       s->u = 0;
diff --git a/bundled/taskflow-3.10.0/taskflow/utility/os.hpp b/bundled/taskflow-3.10.0/taskflow/utility/os.hpp
new file mode 100644 (file)
index 0000000..9a10f6b
--- /dev/null
@@ -0,0 +1,293 @@
+#pragma once
+
+#include <cstdlib>
+#include <cstdio>
+#include <string>
+
+#define TF_OS_LINUX 0
+#define TF_OS_DRAGONFLY 0
+#define TF_OS_FREEBSD 0
+#define TF_OS_NETBSD 0
+#define TF_OS_OPENBSD 0
+#define TF_OS_DARWIN 0
+#define TF_OS_WINDOWS 0
+#define TF_OS_CNK 0
+#define TF_OS_HURD 0
+#define TF_OS_SOLARIS 0
+#define TF_OS_UNIX 0
+
+#ifdef _WIN32
+#undef TF_OS_WINDOWS
+#define TF_OS_WINDOWS 1
+#endif
+
+#ifdef __CYGWIN__
+#undef TF_OS_WINDOWS
+#define TF_OS_WINDOWS 1
+#endif
+
+#if (defined __APPLE__ && defined __MACH__)
+#undef TF_OS_DARWIN
+#define TF_OS_DARWIN 1
+#endif
+
+// in some ppc64 linux installations, only the second condition is met
+#if (defined __linux)
+#undef TF_OS_LINUX
+#define TF_OS_LINUX 1
+#elif (defined __linux__)
+#undef TF_OS_LINUX
+#define TF_OS_LINUX 1
+#else
+#endif
+
+#if (defined __DragonFly__)
+#undef TF_OS_DRAGONFLY
+#define TF_OS_DRAGONFLY 1
+#endif
+
+#if (defined __FreeBSD__)
+#undef TF_OS_FREEBSD
+#define TF_OS_FREEBSD 1
+#endif
+
+#if (defined __NetBSD__)
+#undef TF_OS_NETBSD
+#define TF_OS_NETBSD 1
+#endif
+
+#if (defined __OpenBSD__)
+#undef TF_OS_OPENBSD
+#define TF_OS_OPENBSD 1
+#endif
+
+#if (defined __bgq__)
+#undef TF_OS_CNK
+#define TF_OS_CNK 1
+#endif
+
+#if (defined __GNU__)
+#undef TF_OS_HURD
+#define TF_OS_HURD 1
+#endif
+
+#if (defined __sun)
+#undef TF_OS_SOLARIS
+#define TF_OS_SOLARIS 1
+#endif
+
+#if (1 !=                                                                  \
+     TF_OS_LINUX + TF_OS_DRAGONFLY + TF_OS_FREEBSD + TF_OS_NETBSD +        \
+     TF_OS_OPENBSD + TF_OS_DARWIN + TF_OS_WINDOWS + TF_OS_HURD +           \
+     TF_OS_SOLARIS)
+#define TF_OS_UNKNOWN 1
+#endif
+
+#if TF_OS_LINUX || TF_OS_DRAGONFLY || TF_OS_FREEBSD || TF_OS_NETBSD ||     \
+    TF_OS_OPENBSD || TF_OS_DARWIN || TF_OS_HURD || TF_OS_SOLARIS
+#undef TF_OS_UNIX
+#define TF_OS_UNIX 1
+#endif
+
+
+//-----------------------------------------------------------------------------
+// Cache line alignment
+//-----------------------------------------------------------------------------
+#if defined(__i386__) || defined(__x86_64__)
+  #define TF_CACHELINE_SIZE 64
+#elif defined(__powerpc64__)
+  // This is the L1 D-cache line size of our Power7 machines.
+  // Need to check if this is appropriate for other PowerPC64 systems.
+  #define TF_CACHELINE_SIZE 128
+#elif defined(__arm__)
+  // Cache line sizes for ARM: These values are not strictly correct since
+  // cache line sizes depend on implementations, not architectures.
+  // There are even implementations with cache line sizes configurable
+  // at boot time.
+  #if defined(__ARM_ARCH_5T__)
+    #define TF_CACHELINE_SIZE 32
+  #elif defined(__ARM_ARCH_7A__)
+    #define TF_CACHELINE_SIZE 64
+  #endif
+#endif
+
+#ifndef TF_CACHELINE_SIZE
+// A reasonable default guess.  Note that overestimates tend to waste more
+// space, while underestimates tend to waste more time.
+  #define TF_CACHELINE_SIZE 64
+#endif
+
+
+
+namespace tf {
+
+/**
+ * @brief struct to ensure cacheline-aligned storage for an object.
+ *
+ * This utility struct aligns the stored object `data` to twice the size of a cacheline.
+ * The alignment improves performance by optimizing data access in cache-sensitive scenarios.
+ *
+ * @tparam T The type of the stored object.
+ */
+template <typename T>
+struct CachelineAligned {
+  /**
+   * @brief The stored object, aligned to twice the cacheline size.
+   */
+  alignas (2*TF_CACHELINE_SIZE) T data;
+
+  /**
+   * @brief Provides access to the stored object.
+   * 
+   * @return A reference to the stored object.
+   */
+  T& get() { return data; }
+};
+
+/**
+ * @brief retrieves the value of an environment variable
+ *
+ * This function fetches the value of an environment variable by name.
+ * If the variable is not found, it returns an empty string.
+ *
+ * @param str The name of the environment variable to retrieve.
+ * @return The value of the environment variable as a string, or an empty string if not found.
+ *
+ * @attention The implementation differs between Windows and POSIX platforms:
+ *  - On Windows, it uses `_dupenv_s` to fetch the value.
+ *  - On POSIX, it uses `std::getenv`.
+ *
+ */
+inline std::string get_env(const std::string& str) {
+#ifdef _MSC_VER
+  char *ptr = nullptr;
+  size_t len = 0;
+
+  if(_dupenv_s(&ptr, &len, str.c_str()) == 0 && ptr != nullptr) {
+    std::string res(ptr, len);
+    std::free(ptr);
+    return res;
+  }
+  return "";
+
+#else
+  auto ptr = std::getenv(str.c_str());
+  return ptr ? ptr : "";
+#endif
+}
+
+/**
+ * @brief checks whether an environment variable is defined
+ *
+ * This function determines if a specific environment variable exists in the current environment.
+ *
+ * @param str The name of the environment variable to check.
+ * @return `true` if the environment variable exists, `false` otherwise.
+ *
+ * @attention The implementation differs between Windows and POSIX platforms:
+ *  - On Windows, it uses `_dupenv_s` to check for the variable's presence.
+ *  - On POSIX, it uses `std::getenv` to check for the variable's presence.
+ *
+ */
+inline bool has_env(const std::string& str) {
+#ifdef _MSC_VER
+  char *ptr = nullptr;
+  size_t len = 0;
+
+  if(_dupenv_s(&ptr, &len, str.c_str()) == 0 && ptr != nullptr) {
+    std::string res(ptr, len);
+    std::free(ptr);
+    return true;
+  }
+  return false;
+
+#else
+  auto ptr = std::getenv(str.c_str());
+  return ptr ? true : false;
+#endif
+}
+
+/**
+ * @fn pause
+ * 
+ * This function is used in spin-wait loops to hint the CPU that the current 
+ * thread is in a busy-wait state. 
+ * It helps reduce power consumption and improves performance on hyper-threaded processors 
+ * by preventing the CPU from consuming unnecessary cycles while waiting. 
+ * It is particularly useful in low-contention scenarios, where the thread 
+ * is likely to quickly acquire the lock or condition it's waiting for, 
+ * avoiding an expensive context switch. 
+ * On modern x86 processors, this instruction can be invoked using @c __builtin_ia32_pause() 
+ * in GCC/Clang or @c _mm_pause() in MSVC. 
+ * In non-x86 architectures, alternative mechanisms such as yielding the CPU may be used instead.
+ * 
+ */
+inline void pause() {
+#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
+    // x86 and x86_64: Use the PAUSE instruction
+  #if defined(_MSC_VER)
+    // Microsoft Visual C++
+    _mm_pause();
+  #elif defined(__GNUC__) || defined(__clang__)
+    // GCC and Clang
+    __builtin_ia32_pause();
+  #else
+    asm volatile("pause" ::: "memory");
+  #endif
+
+#elif defined(__aarch64__) || defined(__arm__)
+    // ARM and AArch64: Use the YIELD instruction
+  #if defined(__GNUC__) || defined(__clang__)
+    asm volatile("yield" ::: "memory");
+  #endif
+
+#else
+  // Fallback: Portable yield for unknown architectures
+  std::this_thread::yield();
+#endif
+}
+
+/**
+ * @brief spins until the given predicate becomes true
+ * 
+ * @tparam P the type of the predicate function or callable.
+ * @param predicate the callable that returns a boolean value, which is checked in the loop.
+ * 
+ * This function repeatedly checks the provided predicate in a spin-wait loop
+ * and uses a backoff strategy to minimize CPU waste during the wait. Initially,
+ * it uses the `pause()` instruction for the first 100 iterations to hint to the
+ * CPU that the thread is waiting, thus reducing power consumption and avoiding
+ * unnecessary cycles. After 100 iterations, it switches to yielding the CPU using
+ * `std::this_thread::yield()` to allow other threads to run and improve system
+ * responsiveness.
+ * 
+ * The function operates as follows:
+ * 1. For the first 100 iterations, it invokes `pause()` to reduce power consumption
+ *    during the spin-wait.
+ * 2. After 100 iterations, it uses `std::this_thread::yield()` to relinquish the
+ *    CPU, allowing other threads to execute.
+ * 
+ * @attention This function is useful when you need to wait for a condition to be true, but
+ *       want to optimize CPU usage during the wait by using a busy-wait approach.
+ * 
+ */
+template <typename P>
+void spin_until(P&& predicate) {
+  size_t num_pauses = 0;
+  while(!predicate()) {
+    (num_pauses++ < 100) ? pause() : std::this_thread::yield();
+  }
+}
+
+
+
+}  // end of namespace tf -----------------------------------------------------
+
+
+
+
+
+
+
+
+
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/utility/serializer.hpp
rename to bundled/taskflow-3.10.0/taskflow/utility/serializer.hpp
index aab00f23f4c6511adb1cb6d0ab3fad9761c14d55..5ede84a27641ba7ae69afc099762ea4e18113c95 100644 (file)
@@ -1126,7 +1126,7 @@ SizeType Deserializer<Stream, SizeType>::_load(T&& t) {
   return t.load(*this);
 }
 
-}  // ned of namespace tf -----------------------------------------------------
+}  // end of namespace tf -----------------------------------------------------
 
 
 
similarity index 99%
rename from bundled/taskflow-3.7.0/taskflow/utility/small_vector.hpp
rename to bundled/taskflow-3.10.0/taskflow/utility/small_vector.hpp
index a42c2646a659e4380cb597199a0d9b1145b5466a..0ef8f9c9607325244d6af44836b1451001308002 100644 (file)
@@ -2,6 +2,8 @@
 
 #pragma once
 
+#include "macros.hpp"
+
 #include <algorithm>
 #include <cassert>
 #include <cstddef>
 #include <iterator>
 #include <memory>
 
-#if defined(__GNUC__)
-  #define TF_LIKELY(x) (__builtin_expect((x), 1))
-  #define TF_UNLIKELY(x) (__builtin_expect((x), 0))
-#else
-  #define TF_LIKELY(x) (x)
-  #define TF_UNLIKELY(x) (x)
-#endif
 
 /**
 @file small_vector.hpp
similarity index 94%
rename from bundled/taskflow-3.7.0/taskflow/utility/traits.hpp
rename to bundled/taskflow-3.10.0/taskflow/utility/traits.hpp
index dd3953bd44251f3d52fce9bb2f61e1cb74d885c3..c7addcbaf24a5045135330841efe35b43b6b8f8d 100644 (file)
@@ -1,7 +1,11 @@
 #pragma once
 
 #if __has_include(<version>)
-#  include <version>
+#include <version>
+#endif
+
+#if __has_include(<latch>)
+#include <latch>
 #endif
 
 #include <type_traits>
@@ -296,6 +300,17 @@ using all_same = all_true<std::is_same_v<T, Ts>...>;
 template <typename T, typename... Ts>
 constexpr bool all_same_v = all_same<T, Ts...>::value;
 
+// ----------------------------------------------------------------------------
+// Iterator
+// ----------------------------------------------------------------------------
+
+template <typename I>
+using deref_t = std::decay_t<decltype(*std::declval<I>())>;
+
+template <typename I>
+constexpr auto is_random_access_iterator = std::is_same_v<
+  typename std::iterator_traits<I>::iterator_category, std::random_access_iterator_tag
+>;
 
 }  // end of namespace tf. ----------------------------------------------------
 
diff --git a/bundled/taskflow-3.7.0/LICENSE b/bundled/taskflow-3.7.0/LICENSE
deleted file mode 100644 (file)
index aa56ff9..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-TASKFLOW MIT LICENSE 
-
-Copyright (c) 2018-2024 Dr. Tsung-Wei Huang
-
-The University of Wisconsin at Madison
-
-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/or sell
-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.
diff --git a/bundled/taskflow-3.7.0/taskflow/algorithm/critical.hpp b/bundled/taskflow-3.7.0/taskflow/algorithm/critical.hpp
deleted file mode 100644 (file)
index 46ac395..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-#pragma once
-
-#include "../core/task.hpp"
-
-/**
-@file critical.hpp
-@brief critical include file
-*/
-
-namespace tf {
-
-// ----------------------------------------------------------------------------
-// CriticalSection
-// ----------------------------------------------------------------------------
-
-/**
-@class CriticalSection
-
-@brief class to create a critical region of limited workers to run tasks
-
-tf::CriticalSection is a wrapper over tf::Semaphore and is specialized for
-limiting the maximum concurrency over a set of tasks.
-A critical section starts with an initial count representing that limit.
-When a task is added to the critical section,
-the task acquires and releases the semaphore internal to the critical section.
-This design avoids explicit call of tf::Task::acquire and tf::Task::release.
-The following example creates a critical section of one worker and adds
-the five tasks to the critical section.
-
-@code{.cpp}
-tf::Executor executor(8);   // create an executor of 8 workers
-tf::Taskflow taskflow;
-
-// create a critical section of 1 worker
-tf::CriticalSection critical_section(1);
-
-tf::Task A = taskflow.emplace([](){ std::cout << "A" << std::endl; });
-tf::Task B = taskflow.emplace([](){ std::cout << "B" << std::endl; });
-tf::Task C = taskflow.emplace([](){ std::cout << "C" << std::endl; });
-tf::Task D = taskflow.emplace([](){ std::cout << "D" << std::endl; });
-tf::Task E = taskflow.emplace([](){ std::cout << "E" << std::endl; });
-
-critical_section.add(A, B, C, D, E);
-
-executor.run(taskflow).wait();
-@endcode
-
-*/
-class CriticalSection : public Semaphore {
-
-  public:
-
-    /**
-    @brief constructs a critical region of a limited number of workers
-    */
-    explicit CriticalSection(size_t max_workers = 1);
-
-    /**
-    @brief adds a task into the critical region
-    */
-    template <typename... Tasks>
-    void add(Tasks...tasks);
-};
-
-inline CriticalSection::CriticalSection(size_t max_workers) :
-  Semaphore {max_workers} {
-}
-
-template <typename... Tasks>
-void CriticalSection::add(Tasks... tasks) {
-  (tasks.acquire(*this), ...);
-  (tasks.release(*this), ...);
-}
-
-
-}  // end of namespace tf. ---------------------------------------------------
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/algorithm/for_each.hpp b/bundled/taskflow-3.7.0/taskflow/algorithm/for_each.hpp
deleted file mode 100644 (file)
index 8c98e84..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-#pragma once
-
-#include "launch.hpp"
-
-namespace tf {
-
-// Function: make_for_each_task
-template <typename B, typename E, typename C, typename P = DefaultPartitioner>
-auto make_for_each_task(B b, E e, C c, P part = P()) {
-
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t beg = b;
-    E_t end = e;
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(beg, end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        std::for_each(beg, end, c);
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    // static partitioner
-    if constexpr(part.type() == PartitionerType::STATIC) {
-      size_t chunk_size;
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-        launch_loop(W, w, rt, part, [=, &c, &part] () mutable {
-          part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
-              std::advance(beg, part_b - prev_e);
-              for(size_t x = part_b; x<part_e; x++) {
-                c(*beg++);
-              }
-              prev_e = part_e;
-            }
-          ); 
-        });
-      }
-
-      rt.corun_all();
-    }
-    // dynamic partitioner
-    else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, [=, &c, &next, &part] () mutable {
-        part.loop(N, W, next, 
-          [&, prev_e=size_t{0}](size_t part_b, size_t part_e) mutable {
-            std::advance(beg, part_b - prev_e);
-            for(size_t x = part_b; x<part_e; x++) {
-              c(*beg++);
-            }
-            prev_e = part_e;
-          }
-        );
-      });
-    }
-  };
-}
-
-// Function: make_for_each_index_task
-template <typename B, typename E, typename S, typename C, typename P = DefaultPartitioner>
-auto make_for_each_index_task(B b, E e, S s, C c, P part = P()){
-
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using S_t = std::decay_t<unwrap_ref_decay_t<S>>;
-
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the iterator values
-    B_t beg = b;
-    E_t end = e;
-    S_t inc = s;
-    
-    // nothing to be done if the range is invalid
-    if(is_range_invalid(beg, end, inc)) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = distance(beg, end, inc);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        for(size_t x=0; x<N; x++, beg+=inc) {
-          c(beg);
-        }
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    // static partitioner
-    if constexpr(part.type() == PartitionerType::STATIC) {
-      size_t chunk_size;
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-        launch_loop(W, w, rt, part, [=, &c, &part] () mutable {
-          part.loop(N, W, curr_b, chunk_size,
-            [&](size_t part_b, size_t part_e) {
-              auto idx = static_cast<B_t>(part_b) * inc + beg;
-              for(size_t x=part_b; x<part_e; x++, idx += inc) {
-                c(idx);
-              }
-            }
-          );
-        });
-      }
-
-      rt.corun_all();
-    }
-    // dynamic partitioner
-    else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, [=, &c, &next, &part] () mutable {
-        part.loop(N, W, next, 
-          [&](size_t part_b, size_t part_e) {
-            auto idx = static_cast<B_t>(part_b) * inc + beg;
-            for(size_t x=part_b; x<part_e; x++, idx += inc) {
-              c(idx);
-            }
-          }
-        );
-      });
-    }
-  };
-}
-
-// ----------------------------------------------------------------------------
-// for_each
-// ----------------------------------------------------------------------------
-
-// Function: for_each
-template <typename B, typename E, typename C, typename P>
-Task FlowBuilder::for_each(B beg, E end, C c, P part) {
-  return emplace(
-    make_for_each_task(beg, end, c, part)
-  );
-}
-
-// ----------------------------------------------------------------------------
-// for_each_index
-// ----------------------------------------------------------------------------
-
-// Function: for_each_index
-template <typename B, typename E, typename S, typename C, typename P>
-Task FlowBuilder::for_each_index(B beg, E end, S inc, C c, P part){
-  return emplace(
-    make_for_each_index_task(beg, end, inc, c, part)
-  );
-}
-
-
-}  // end of namespace tf -----------------------------------------------------
-
diff --git a/bundled/taskflow-3.7.0/taskflow/algorithm/launch.hpp b/bundled/taskflow-3.7.0/taskflow/algorithm/launch.hpp
deleted file mode 100644 (file)
index 527fb2f..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-#pragma once
-
-#include <functional>
-#include "../core/async.hpp"
-
-namespace tf {
-
-// Function: launch_loop
-template<typename P, typename Loop>
-TF_FORCE_INLINE void launch_loop(P part, Loop loop) {
-
-  constexpr bool is_default_wrapper_v = std::is_same_v<
-    typename std::decay_t<P>::closure_wrapper_type, DefaultClosureWrapper
-  >;
-  
-  if constexpr(is_default_wrapper_v) {
-    loop();
-  }
-  else {
-    std::invoke(part.closure_wrapper(), loop);
-  }
-}
-
-// Function: launch_loop
-template <typename P, typename Loop>
-TF_FORCE_INLINE void launch_loop(
-  size_t N, 
-  size_t W, 
-  Runtime& rt, 
-  std::atomic<size_t>& next, 
-  P part, 
-  Loop loop
-) {
-
-  //static_assert(std::is_lvalue_reference_v<Loop>, "");
-  
-  using namespace std::string_literals;
-
-  for(size_t w=0; w<W; w++) {
-    auto r = N - next.load(std::memory_order_relaxed);
-    // no more loop work to do - finished by previous async tasks
-    if(!r) {
-      break;
-    }
-    // tail optimization
-    if(r <= part.chunk_size() || w == W-1) {
-      launch_loop(part, loop);
-      break;
-    }
-    else {
-      rt.silent_async_unchecked([=](){ launch_loop(part, loop); });
-    }
-  }
-      
-  rt.corun_all();
-}
-
-// Function: launch_loop
-template <typename P, typename Loop>
-TF_FORCE_INLINE void launch_loop(
-  size_t W,
-  size_t w,
-  Runtime& rt, 
-  P part,
-  Loop loop 
-) {
-  using namespace std::string_literals;
-  if(w == W-1) {
-    launch_loop(part, loop);
-  }
-  else {
-    rt.silent_async_unchecked([=](){ launch_loop(part, loop); });
-  }
-}
-
-}  // end of namespace tf -----------------------------------------------------
diff --git a/bundled/taskflow-3.7.0/taskflow/algorithm/reduce.hpp b/bundled/taskflow-3.7.0/taskflow/algorithm/reduce.hpp
deleted file mode 100644 (file)
index b280934..0000000
+++ /dev/null
@@ -1,434 +0,0 @@
-#pragma once
-
-#include "launch.hpp"
-
-namespace tf {
-
-// Function: make_reduce_task
-template <typename B, typename E, typename T, typename O, typename P = DefaultPartitioner>
-auto make_reduce_task(B b, E e, T& init, O bop, P part = P()) {
-
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-
-  return [=, &r=init] (Runtime& rt) mutable {
-
-    // fetch the iterator values
-    B_t beg = b;
-    E_t end = e;
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(beg, end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        for(; beg!=end; r = bop(r, *beg++));
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-
-    std::mutex mtx;
-
-    // static partitioner
-    if constexpr(part.type() == PartitionerType::STATIC) {
-      
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-        
-        // we force chunk size to be at least two because the temporary
-        // variable sum need to avoid copy at the first step
-        chunk_size = std::max(size_t{2}, part.adjusted_chunk_size(N, W, w));
-        
-        launch_loop(W, w, rt, part, [=, &bop, &mtx, &r, &part] () mutable {
-
-          std::advance(beg, curr_b);
-
-          if(N - curr_b == 1) {
-            std::lock_guard<std::mutex> lock(mtx);
-            r = bop(r, *beg);
-            return;
-          }
-
-          auto beg1 = beg++;
-          auto beg2 = beg++;
-          T sum = bop(*beg1, *beg2);
-        
-          // loop reduce
-          part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=curr_b+2](size_t part_b, size_t part_e) mutable {
-
-              if(part_b > prev_e) {
-                std::advance(beg, part_b - prev_e);
-              }
-              else {
-                part_b = prev_e;
-              }
-
-              for(size_t x=part_b; x<part_e; x++, beg++) {
-                sum = bop(sum, *beg);
-              }
-              prev_e = part_e;
-            }
-          ); 
-          
-          // final reduce
-          std::lock_guard<std::mutex> lock(mtx);
-          r = bop(r, sum);
-        });
-      }
-      rt.corun_all();
-    }
-    // dynamic partitioner
-    else {
-      std::atomic<size_t> next(0);
-      launch_loop(N, W, rt, next, part, [=, &bop, &mtx, &next, &r, &part] () mutable {
-        // pre-reduce
-        size_t s0 = next.fetch_add(2, std::memory_order_relaxed);
-
-        if(s0 >= N) {
-          return;
-        }
-
-        std::advance(beg, s0);
-
-        if(N - s0 == 1) {
-          std::lock_guard<std::mutex> lock(mtx);
-          r = bop(r, *beg);
-          return;
-        }
-
-        auto beg1 = beg++;
-        auto beg2 = beg++;
-
-        T sum = bop(*beg1, *beg2);
-        
-        // loop reduce
-        part.loop(N, W, next, 
-          [&, prev_e=s0+2](size_t curr_b, size_t curr_e) mutable {
-            std::advance(beg, curr_b - prev_e);
-            for(size_t x=curr_b; x<curr_e; x++, beg++) {
-              sum = bop(sum, *beg);
-            }
-            prev_e = curr_e;
-          }
-        ); 
-        
-        // final reduce
-        std::lock_guard<std::mutex> lock(mtx);
-        r = bop(r, sum);
-      });
-    }
-  };
-}
-
-// Function: make_transform_reduce_task
-template <
-  typename B, typename E, typename T, typename BOP, typename UOP, 
-  typename P = DefaultPartitioner
->
-auto make_transform_reduce_task(B b, E e, T& init, BOP bop, UOP uop, P part = P()) {
-
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-
-  return [=, &r=init] (Runtime& rt) mutable {
-
-    // fetch the iterator values
-    B_t beg = b;
-    E_t end = e;
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(beg, end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        for(; beg!=end; r = bop(std::move(r), uop(*beg++))); 
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-
-    std::mutex mtx;
-    
-    // static partitioner
-    if constexpr(part.type() == PartitionerType::STATIC) {
-      
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-      
-        chunk_size = part.adjusted_chunk_size(N, W, w);
-
-        launch_loop(W, w, rt, part, [=, &bop, &uop, &mtx, &r, &part] () mutable {
-          std::advance(beg, curr_b);
-
-          if(N - curr_b == 1) {
-            std::lock_guard<std::mutex> lock(mtx);
-            r = bop(std::move(r), uop(*beg));
-            return;
-          }
-
-          //auto beg1 = beg++;
-          //auto beg2 = beg++;
-          //T sum = bop(uop(*beg1), uop(*beg2));
-
-          T sum = (chunk_size == 1) ? uop(*beg++) : bop(uop(*beg++), uop(*beg++));
-        
-          // loop reduce
-          part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=curr_b+(chunk_size == 1 ? 1 : 2)]
-            (size_t part_b, size_t part_e) mutable {
-              if(part_b > prev_e) {
-                std::advance(beg, part_b - prev_e);
-              }
-              else {
-                part_b = prev_e;
-              }
-              for(size_t x=part_b; x<part_e; x++, beg++) {
-                sum = bop(std::move(sum), uop(*beg));
-              }
-              prev_e = part_e;
-            }
-          ); 
-          
-          // final reduce
-          std::lock_guard<std::mutex> lock(mtx);
-          r = bop(std::move(r), std::move(sum));
-        });
-      }
-      
-      rt.corun_all();
-    }
-    // dynamic partitioner
-    else {
-      std::atomic<size_t> next(0);
-        
-      launch_loop(N, W, rt, next, part, [=, &bop, &uop, &mtx, &next, &r, &part] () mutable {
-        // pre-reduce
-        size_t s0 = next.fetch_add(2, std::memory_order_relaxed);
-
-        if(s0 >= N) {
-          return;
-        }
-
-        std::advance(beg, s0);
-
-        if(N - s0 == 1) {
-          std::lock_guard<std::mutex> lock(mtx);
-          r = bop(std::move(r), uop(*beg));
-          return;
-        }
-
-        auto beg1 = beg++;
-        auto beg2 = beg++;
-
-        T sum = bop(uop(*beg1), uop(*beg2));
-        
-        // loop reduce
-        part.loop(N, W, next, 
-          [&, prev_e=s0+2](size_t curr_b, size_t curr_e) mutable {
-            std::advance(beg, curr_b - prev_e);
-            for(size_t x=curr_b; x<curr_e; x++, beg++) {
-              sum = bop(std::move(sum), uop(*beg));
-            }
-            prev_e = curr_e;
-          }
-        ); 
-        
-        // final reduce
-        std::lock_guard<std::mutex> lock(mtx);
-        r = bop(std::move(r), std::move(sum));
-      });
-    }
-  };
-}
-
-// Function: make_transform_reduce_task with two binary operation
-template <
-  typename B1, typename E1, typename B2, typename T, typename BOP_R, typename BOP_T, 
-  typename P = DefaultPartitioner,
-  std::enable_if_t<!is_partitioner_v<std::decay_t<BOP_T>>, void>* = nullptr
->
-auto make_transform_reduce_task(
-  B1 b1, E1 e1, B2 b2, T& init, BOP_R bop_r, BOP_T bop_t, P part = P()
-) {
-
-  using B1_t = std::decay_t<unwrap_ref_decay_t<B1>>;
-  using E1_t = std::decay_t<unwrap_ref_decay_t<E1>>;
-  using B2_t = std::decay_t<unwrap_ref_decay_t<B2>>;
-
-  return [=, &r=init] (Runtime& rt) mutable {
-
-    // fetch the iterator values
-    B1_t beg1 = b1;
-    E1_t end1 = e1;
-    B2_t beg2 = b2; 
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(beg1, end1);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= part.chunk_size()) {
-      launch_loop(part, [&](){
-        for(; beg1!=end1; r = bop_r(std::move(r), bop_t(*beg1++, *beg2++)));
-      });
-      return;
-    }   
-
-    if(N < W) {
-      W = N;
-    }   
-
-    std::mutex mtx;
-    
-    // static partitioner
-    if constexpr(part.type() == PartitionerType::STATIC) {
-    
-      size_t chunk_size;
-
-      for(size_t w=0, curr_b=0; w<W && curr_b < N; ++w, curr_b += chunk_size) {
-    
-        chunk_size = part.adjusted_chunk_size(N, W, w); 
-
-        launch_loop(W, w, rt, part, [=, &bop_r, &bop_t, &mtx, &r, &part] () mutable {
-          std::advance(beg1, curr_b);
-          std::advance(beg2, curr_b);
-
-          if(N - curr_b == 1) {
-            std::lock_guard<std::mutex> lock(mtx);
-            r = bop_r(std::move(r), bop_t(*beg1, *beg2));
-            return;
-          }   
-
-          T sum = (chunk_size == 1) ? bop_t(*beg1++, *beg2++) : 
-            bop_r(bop_t(*beg1++, *beg2++), bop_t(*beg1++, *beg2++));
-    
-          // loop reduce
-          part.loop(N, W, curr_b, chunk_size,
-            [&, prev_e=curr_b+(chunk_size == 1 ? 1 : 2)] 
-            (size_t part_b, size_t part_e) mutable {
-              if(part_b > prev_e) {
-                std::advance(beg1, part_b - prev_e);
-                std::advance(beg2, part_b - prev_e);
-              }   
-              else {
-                part_b = prev_e;
-              }   
-              for(size_t x=part_b; x<part_e; x++, beg1++, beg2++) { 
-                sum = bop_r(std::move(sum), bop_t(*beg1, *beg2));
-              }   
-              prev_e = part_e;
-            }   
-          );  
-    
-          // final reduce
-          std::lock_guard<std::mutex> lock(mtx);
-          r = bop_r(std::move(r), std::move(sum));
-        }); 
-      }   
-    
-      rt.corun_all();
-    }   
-    // dynamic partitioner
-    else {
-      std::atomic<size_t> next(0);
-    
-      launch_loop(N, W, rt, next, part, [=, &bop_r, &bop_t, &mtx, &next, &r, &part] () mutable {
-        // pre-reduce
-        size_t s0 = next.fetch_add(2, std::memory_order_relaxed);
-
-        if(s0 >= N) {
-          return;
-        }   
-
-        std::advance(beg1, s0);
-        std::advance(beg2, s0);
-
-        if(N - s0 == 1) {
-          std::lock_guard<std::mutex> lock(mtx);
-          r = bop_r(std::move(r), bop_t(*beg1, *beg2));
-          return;
-        }   
-
-        auto beg11 = beg1++;
-        auto beg12 = beg1++;
-        auto beg21 = beg2++;
-        auto beg22 = beg2++;
-
-        T sum = bop_r(bop_t(*beg11, *beg21), bop_t(*beg12, *beg22));
-
-        // loop reduce
-        part.loop(N, W, next, 
-          [&, prev_e=s0+2](size_t curr_b, size_t curr_e) mutable {
-            std::advance(beg1, curr_b - prev_e);
-            std::advance(beg2, curr_b - prev_e);
-            for(size_t x=curr_b; x<curr_e; x++, beg1++, beg2++) {
-              sum = bop_r(std::move(sum), bop_t(*beg1, *beg2));
-            }   
-            prev_e = curr_e;
-          }   
-        );  
-    
-        // final reduce
-        std::lock_guard<std::mutex> lock(mtx);
-        r = bop_r(std::move(r), std::move(sum));
-      }); 
-    }   
-  };  
-}
-
-// ----------------------------------------------------------------------------
-// default reduction
-// ----------------------------------------------------------------------------
-
-// Function: reduce
-template <typename B, typename E, typename T, typename O, typename P>
-Task FlowBuilder::reduce(B beg, E end, T& init, O bop, P part) {
-  return emplace(make_reduce_task(beg, end, init, bop, part));
-}
-
-// ----------------------------------------------------------------------------
-// default transform and reduction
-// ----------------------------------------------------------------------------
-
-// Function: transform_reduce
-template <typename B, typename E, typename T, typename BOP, typename UOP, typename P,
-  std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>*
->
-Task FlowBuilder::transform_reduce(
-  B beg, E end, T& init, BOP bop, UOP uop, P part
-) {
-  return emplace(make_transform_reduce_task(
-    beg, end, init, bop, uop, part
-  ));
-}
-
-// Function: transform_reduce
-template <
-  typename B1, typename E1, typename B2, typename T, typename BOP_R, typename BOP_T, 
-  typename P,
-  std::enable_if_t<!is_partitioner_v<std::decay_t<BOP_T>>, void>*
->
-Task FlowBuilder::transform_reduce(
-  B1 beg1, E1 end1, B2 beg2, T& init, BOP_R bop_r, BOP_T bop_t, P part
-) {
-  return emplace(make_transform_reduce_task(
-    beg1, end1, beg2, init, bop_r, bop_t, part
-  ));
-}
-
-}  // end of namespace tf -----------------------------------------------------
-
-
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/algorithm/scan.hpp b/bundled/taskflow-3.7.0/taskflow/algorithm/scan.hpp
deleted file mode 100644 (file)
index c168212..0000000
+++ /dev/null
@@ -1,633 +0,0 @@
-#pragma once
-
-#include "launch.hpp"
-
-namespace tf {
-
-namespace detail {
-
-// Function: scan_loop
-template <typename Iterator, typename BufferT, typename B>
-void scan_loop(
-  tf::Runtime& rt,
-  std::atomic<size_t>& counter, 
-  BufferT& buf, 
-  B bop, 
-  Iterator d_beg, 
-  size_t W,
-  size_t w, 
-  size_t chunk_size
-){
-  // whoever finishes the last performs global scan
-  if(counter.fetch_add(1, std::memory_order_acq_rel) == W-1) {
-    for(size_t i=1; i<buf.size(); i++) {
-      buf[i].data = bop(buf[i-1].data, buf[i].data);
-    }
-    counter.store(0, std::memory_order_release);
-  }
-
-  // first worker no need to do any work
-  if(w==0) {
-    return;
-  } 
-
-  // need to do public corun because multiple workers can call this
-  rt.executor().corun_until([&counter](){
-    return counter.load(std::memory_order_acquire) == 0;
-  });
-  
-  // block addup
-  for(size_t i=0; i<chunk_size; i++) {
-    *d_beg++ = bop(buf[w-1].data, *d_beg);
-  }
-}
-
-}  // end of namespace tf::detail ---------------------------------------------
-
-
-// Function: make_inclusive_scan_task
-template <typename B, typename E, typename D, typename BOP, typename P = DefaultPartitioner,
-  std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>* = nullptr
->
-auto make_inclusive_scan_task(
-  B first, E last, D d_first, BOP bop, P part = P()
-) {
-  
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
-  using value_type = typename std::iterator_traits<B_t>::value_type;
-  
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t s_beg = first;
-    E_t s_end = last;
-    D_t d_beg = d_first;
-
-    if(s_beg == s_end) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(s_beg, s_end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= 2) {
-      launch_loop(part, [&](){
-        std::inclusive_scan(s_beg, s_end, d_beg, bop);
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    std::vector<CachelineAligned<value_type>> buf(W);
-    std::atomic<size_t> counter(0);
-
-    size_t Q = N/W;
-    size_t R = N%W;
-    
-    //auto orig_d_beg = d_beg;
-    //ExecutionPolicy<StaticPartitioner> policy;
-
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-
-      chunk_size = std::min(Q + (w < R), N - curr_b);
-
-      // block scan
-      launch_loop(W, w, rt, part, [=, &rt, &bop, &buf, &counter] () mutable {
-        auto result = d_beg;
-
-        // local scan per worker
-        auto& init = buf[w].data;
-        *d_beg++ = init = *s_beg++;
-
-        for(size_t i=1; i<chunk_size; i++){
-          *d_beg++ = init = bop(init, *s_beg++); 
-        }
-
-        // block scan
-        detail::scan_loop(rt, counter, buf, bop, result, W, w, chunk_size);
-        
-        //size_t offset = R ? Q + 1 : Q;
-        //size_t rest   = N - offset;
-        //size_t rest_Q = rest / W;
-        //size_t rest_R = rest % W;
-        //
-        //chunk_size = policy.chunk_size() == 0 ? 
-        //             rest_Q + (w < rest_R) : policy.chunk_size();
-        //
-        //size_t curr_b = policy.chunk_size() == 0 ? 
-        //                offset + (w<rest_R ? w*(rest_Q + 1) : rest_R + w*rest_Q) :
-        //                offset + w*policy.chunk_size();
-
-        //policy(N, W, curr_b, chunk_size,
-        //  [&, prev_e=size_t{0}](size_t curr_b, size_t curr_e) mutable {
-        //    std::advance(orig_d_beg, curr_b - prev_e);
-        //    for(size_t x = curr_b; x<curr_e; x++) {
-        //      size_t j = x < (Q+1)*R ? x/(Q+1) : (x-(Q+1)*R)/Q + R;
-        //      *orig_d_beg++ = bop(buf[j-1].data, *orig_d_beg);
-        //    }
-        //    prev_e = curr_e;
-        //  }
-        //);
-      });
-      
-      std::advance(s_beg, chunk_size);
-      std::advance(d_beg, chunk_size);
-      curr_b += chunk_size;
-    }
-
-    rt.corun_all();
-  };
-}
-
-// Function: make_inclusive_scan_task
-template <typename B, typename E, typename D, typename BOP, typename T, typename P = DefaultPartitioner,
-  std::enable_if_t<!is_partitioner_v<std::decay_t<T>>, void>* = nullptr
->
-auto make_inclusive_scan_task(
-  B first, E last, D d_first, BOP bop, T init, P part = P()
-) {
-  
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
-  using value_type = typename std::iterator_traits<B_t>::value_type;
-  
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t s_beg = first;
-    E_t s_end = last;
-    D_t d_beg = d_first;
-
-    if(s_beg == s_end) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(s_beg, s_end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= 2) {
-      launch_loop(part, [&](){
-        std::inclusive_scan(s_beg, s_end, d_beg, bop, init);
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    std::vector<CachelineAligned<value_type>> buf(W);
-    std::atomic<size_t> counter(0);
-    
-    // set up the initial value for the first worker
-    buf[0].data = std::move(init);
-
-    size_t Q = N/W;
-    size_t R = N%W;
-
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-
-      chunk_size = std::min(Q + (w < R), N - curr_b);
-
-      // block scan
-      launch_loop(W, w, rt, part, [=, &rt, &bop, &buf, &counter] () mutable {
-        auto result = d_beg;
-
-        // local scan per worker
-        auto& local = buf[w].data;
-        *d_beg++ = local = (w == 0) ? bop(local, *s_beg++) : *s_beg++;
-
-        for(size_t i=1; i<chunk_size; i++){
-          *d_beg++ = local = bop(local, *s_beg++); 
-        }
-        
-        // block scan
-        detail::scan_loop(rt, counter, buf, bop, result, W, w, chunk_size);
-      });
-
-      std::advance(s_beg, chunk_size);
-      std::advance(d_beg, chunk_size);
-      curr_b += chunk_size;
-    }
-
-    rt.corun_all();
-  };
-}
-
-// ----------------------------------------------------------------------------
-// Transform Inclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: transform_inclusive_scan
-template <typename B, typename E, typename D, typename BOP, typename UOP, typename P = DefaultPartitioner,
-  std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>* = nullptr
->
-auto make_transform_inclusive_scan_task(
-  B first, E last, D d_first, BOP bop, UOP uop, P part = P()
-) {
-  
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
-  using value_type = typename std::iterator_traits<B_t>::value_type;
-  
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t s_beg = first;
-    E_t s_end = last;
-    D_t d_beg = d_first;
-
-    if(s_beg == s_end) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(s_beg, s_end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= 2) {
-      launch_loop(part, [&](){
-        std::transform_inclusive_scan(s_beg, s_end, d_beg, bop, uop); 
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    std::vector<CachelineAligned<value_type>> buf(W);
-    std::atomic<size_t> counter(0);
-
-    size_t Q = N/W;
-    size_t R = N%W;
-    
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-
-      chunk_size = std::min(Q + (w < R), N - curr_b);
-
-      // block scan
-      launch_loop(W, w, rt, part, [=, &rt, &bop, &uop, &buf, &counter] () mutable {
-        auto result = d_beg;
-
-        // local scan per worker
-        auto& init = buf[w].data;
-        *d_beg++ = init = uop(*s_beg++);
-
-        for(size_t i=1; i<chunk_size; i++){
-          *d_beg++ = init = bop(init, uop(*s_beg++)); 
-        }
-
-        // block scan
-        detail::scan_loop(rt, counter, buf, bop, result, W, w, chunk_size);
-      });
-      
-      std::advance(s_beg, chunk_size);
-      std::advance(d_beg, chunk_size);
-      curr_b += chunk_size;
-    }
-
-    rt.corun_all();
-  };
-}
-
-// Function: transform_inclusive_scan
-template <typename B, typename E, typename D, typename BOP, typename UOP, typename T, typename P = DefaultPartitioner,
-  std::enable_if_t<!is_partitioner_v<std::decay_t<T>>, void>* = nullptr
->
-auto make_transform_inclusive_scan_task(
-  B first, E last, D d_first, BOP bop, UOP uop, T init, P part = P()
-) {
-  
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
-  using value_type = typename std::iterator_traits<B_t>::value_type;
-  
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t s_beg = first;
-    E_t s_end = last;
-    D_t d_beg = d_first;
-
-    if(s_beg == s_end) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(s_beg, s_end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= 2) {
-      launch_loop(part, [&](){
-        std::transform_inclusive_scan(s_beg, s_end, d_beg, bop, uop, init);
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    std::vector<CachelineAligned<value_type>> buf(W);
-    std::atomic<size_t> counter(0);
-    
-    // set up the initial value for the first worker
-    buf[0].data = std::move(init);
-
-    size_t Q = N/W;
-    size_t R = N%W;
-
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-
-      chunk_size = std::min(Q + (w < R), N - curr_b);
-
-      // block scan
-      launch_loop(W, w, rt, part, [=, &rt, &bop, &uop, &buf, &counter] () mutable {
-        auto result = d_beg;
-
-        // local scan per worker
-        auto& local = buf[w].data;
-        *d_beg++ = local = (w == 0) ? bop(local, uop(*s_beg++)) : uop(*s_beg++);
-
-        for(size_t i=1; i<chunk_size; i++){
-          *d_beg++ = local = bop(local, uop(*s_beg++)); 
-        }
-        
-        // block scan
-        detail::scan_loop(rt, counter, buf, bop, result, W, w, chunk_size);
-      });
-
-      std::advance(s_beg, chunk_size);
-      std::advance(d_beg, chunk_size);
-      curr_b += chunk_size;
-    }
-
-    rt.corun_all();
-    
-  };
-}
-
-// ----------------------------------------------------------------------------
-// Exclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: make_exclusive_scan_task
-template <typename B, typename E, typename D, typename T, typename BOP, typename P = DefaultPartitioner>
-auto make_exclusive_scan_task(
-  B first, E last, D d_first, T init, BOP bop, P part = P()
-) {
-  
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
-  using value_type = typename std::iterator_traits<B_t>::value_type;
-  
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t s_beg = first;
-    E_t s_end = last;
-    D_t d_beg = d_first;
-
-    if(s_beg == s_end) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(s_beg, s_end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= 2) {
-      launch_loop(part, [&](){
-        std::exclusive_scan(s_beg, s_end, d_beg, init, bop);
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    std::vector<CachelineAligned<value_type>> buf(W);
-    std::atomic<size_t> counter(0);
-
-    size_t Q = N/W;
-    size_t R = N%W;
-
-    // fetch the init value
-    auto s_beg_temp = s_beg;
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-      chunk_size = std::min(Q + (w<R), N - curr_b);  
-      buf[w].data = w ? *s_beg_temp : std::move(init);
-      std::advance(s_beg_temp, chunk_size - !w);
-      curr_b += chunk_size;
-    }
-    
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-
-      chunk_size = std::min(Q + (w < R), N - curr_b);
-
-      // block scan
-      launch_loop(W, w, rt, part, [=, &rt, &bop, &buf, &counter] () mutable {
-        auto result = d_beg;
-
-        // local scan per worker
-        auto& local = buf[w].data;
-
-        for(size_t i=1; i<chunk_size; i++) {
-          auto v = local;
-          local = bop(local, *s_beg++);
-          *d_beg++ = std::move(v);
-        }
-        *d_beg++ = local;
-        
-        // block scan
-        detail::scan_loop(rt, counter, buf, bop, result, W, w, chunk_size);
-      });
-      
-      std::advance(s_beg, chunk_size);
-      std::advance(d_beg, chunk_size);
-      curr_b += chunk_size;
-    }
-
-    rt.corun_all();
-    
-  };
-}
-
-// ----------------------------------------------------------------------------
-// Transform Exclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: 
-template <typename B, typename E, typename D, typename T, typename BOP, typename UOP, typename P = DefaultPartitioner>
-auto make_transform_exclusive_scan_task(
-  B first, E last, D d_first, T init, BOP bop, UOP uop, P part = P()
-) {
-  
-  using B_t = std::decay_t<unwrap_ref_decay_t<B>>;
-  using E_t = std::decay_t<unwrap_ref_decay_t<E>>;
-  using D_t = std::decay_t<unwrap_ref_decay_t<D>>;
-  using value_type = typename std::iterator_traits<B_t>::value_type;
-  
-  return [=] (Runtime& rt) mutable {
-
-    // fetch the stateful values
-    B_t s_beg = first;
-    E_t s_end = last;
-    D_t d_beg = d_first;
-
-    if(s_beg == s_end) {
-      return;
-    }
-
-    size_t W = rt.executor().num_workers();
-    size_t N = std::distance(s_beg, s_end);
-
-    // only myself - no need to spawn another graph
-    if(W <= 1 || N <= 2) {
-      launch_loop(part, [&](){
-        std::transform_exclusive_scan(s_beg, s_end, d_beg, init, bop, uop);
-      });
-      return;
-    }
-
-    if(N < W) {
-      W = N;
-    }
-    
-    std::vector<CachelineAligned<value_type>> buf(W);
-    std::atomic<size_t> counter(0);
-
-    size_t Q = N/W;
-    size_t R = N%W;
-
-    // fetch the init value
-    auto s_beg_temp = s_beg;
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-      chunk_size = std::min(Q + (w<R), N - curr_b);  
-      buf[w].data = w ? uop(*s_beg_temp) : std::move(init);
-      std::advance(s_beg_temp, chunk_size - !w);
-      curr_b += chunk_size;
-    }
-    
-    for(size_t w=0, curr_b=0, chunk_size; w<W && curr_b < N; ++w) {
-
-      chunk_size = std::min(Q + (w < R), N - curr_b);
-
-      // block scan
-      launch_loop(W, w, rt, part, [=, &rt, &bop, &uop, &buf, &counter] () mutable {
-        auto result = d_beg;
-
-        // local scan per worker
-        auto& local = buf[w].data;
-
-        for(size_t i=1; i<chunk_size; i++) {
-          auto v = local;
-          local = bop(local, uop(*s_beg++));
-          *d_beg++ = std::move(v);
-        }
-        *d_beg++ = local;
-        
-        // block scan
-        detail::scan_loop(rt, counter, buf, bop, result, W, w, chunk_size);
-      });
-      
-      std::advance(s_beg, chunk_size);
-      std::advance(d_beg, chunk_size);
-      curr_b += chunk_size;
-    }
-
-    rt.corun_all();
-    
-  };
-}
-
-
-// ----------------------------------------------------------------------------
-// Inclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: inclusive_scan
-template <typename B, typename E, typename D, typename BOP, typename P,
-  std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>*
->
-Task FlowBuilder::inclusive_scan(B first, E last, D d_first, BOP bop, P part) {
-  return emplace(make_inclusive_scan_task(first, last, d_first, bop, part));
-}
-
-// Function: inclusive_scan
-template <typename B, typename E, typename D, typename BOP, typename T, typename P,
-  std::enable_if_t<!is_partitioner_v<std::decay_t<T>>, void>*
->
-Task FlowBuilder::inclusive_scan(B first, E last, D d_first, BOP bop, T init, P part) {
-  return emplace(make_inclusive_scan_task(first, last, d_first, bop, init, part));
-}
-
-// ----------------------------------------------------------------------------
-// Transform Inclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: transform_inclusive_scan
-template <typename B, typename E, typename D, typename BOP, typename UOP, typename P,
-  std::enable_if_t<is_partitioner_v<std::decay_t<P>>, void>*
->
-Task FlowBuilder::transform_inclusive_scan(
-  B first, E last, D d_first, BOP bop, UOP uop, P part
-) {
-  return emplace(make_transform_inclusive_scan_task(
-    first, last, d_first, bop, uop, part
-  ));
-}
-
-// Function: transform_inclusive_scan
-template <typename B, typename E, typename D, typename BOP, typename UOP, typename T, typename P,
-  std::enable_if_t<!is_partitioner_v<std::decay_t<T>>, void>*
->
-Task FlowBuilder::transform_inclusive_scan(
-  B first, E last, D d_first, BOP bop, UOP uop, T init, P part
-) {
-  return emplace(make_transform_inclusive_scan_task(
-    first, last, d_first, bop, uop, init, part
-  ));  
-}
-
-// ----------------------------------------------------------------------------
-// Exclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: exclusive_scan
-template <typename B, typename E, typename D, typename T, typename BOP, typename P>
-Task FlowBuilder::exclusive_scan(B first, E last, D d_first, T init, BOP bop, P part) {
-  return emplace(make_exclusive_scan_task(
-    first, last, d_first, init, bop, part
-  ));
-}
-
-// ----------------------------------------------------------------------------
-// Transform Exclusive Scan
-// ----------------------------------------------------------------------------
-
-// Function: transform_exclusive_scan
-template <typename B, typename E, typename D, typename T, typename BOP, typename UOP, typename P>
-Task FlowBuilder::transform_exclusive_scan(
-  B first, E last, D d_first, T init, BOP bop, UOP uop, P part
-) {
-  return emplace(make_transform_exclusive_scan_task(
-    first, last, d_first, init, bop, uop, part
-  )); 
-}
-
-}  // end of namespace tf -----------------------------------------------------
-
diff --git a/bundled/taskflow-3.7.0/taskflow/core/error.hpp b/bundled/taskflow-3.7.0/taskflow/core/error.hpp
deleted file mode 100644 (file)
index 6a68bea..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-#pragma once
-
-#include <iostream>
-#include <sstream>
-#include <exception>
-
-#include "../utility/stream.hpp"
-
-namespace tf {
-
-// Procedure: throw_se
-// Throws the system error under a given error code.
-template <typename... ArgsT>
-//void throw_se(const char* fname, const size_t line, Error::Code c, ArgsT&&... args) {
-void throw_re(const char* fname, const size_t line, ArgsT&&... args) {
-  std::ostringstream oss;
-  oss << "[" << fname << ":" << line << "] ";
-  //ostreamize(oss, std::forward<ArgsT>(args)...);
-  (oss << ... << args);
-  throw std::runtime_error(oss.str());
-}
-
-}  // ------------------------------------------------------------------------
-
-#define TF_THROW(...) tf::throw_re(__FILE__, __LINE__, __VA_ARGS__);
-
diff --git a/bundled/taskflow-3.7.0/taskflow/core/executor-module-opt.hpp b/bundled/taskflow-3.7.0/taskflow/core/executor-module-opt.hpp
deleted file mode 100644 (file)
index 0e2b1ee..0000000
+++ /dev/null
@@ -1,2025 +0,0 @@
-#pragma once
-
-#include "observer.hpp"
-#include "taskflow.hpp"
-
-/**
-@file executor.hpp
-@brief executor include file
-*/
-
-namespace tf {
-
-// ----------------------------------------------------------------------------
-// Executor Definition
-// ----------------------------------------------------------------------------
-
-/** @class Executor
-
-@brief class to create an executor for running a taskflow graph
-
-An executor manages a set of worker threads to run one or multiple taskflows
-using an efficient work-stealing scheduling algorithm.
-
-@code{.cpp}
-// Declare an executor and a taskflow
-tf::Executor executor;
-tf::Taskflow taskflow;
-
-// Add three tasks into the taskflow
-tf::Task A = taskflow.emplace([] () { std::cout << "This is TaskA\n"; });
-tf::Task B = taskflow.emplace([] () { std::cout << "This is TaskB\n"; });
-tf::Task C = taskflow.emplace([] () { std::cout << "This is TaskC\n"; });
-
-// Build precedence between tasks
-A.precede(B, C);
-
-tf::Future<void> fu = executor.run(taskflow);
-fu.wait();                // block until the execution completes
-
-executor.run(taskflow, [](){ std::cout << "end of 1 run"; }).wait();
-executor.run_n(taskflow, 4);
-executor.wait_for_all();  // block until all associated executions finish
-executor.run_n(taskflow, 4, [](){ std::cout << "end of 4 runs"; }).wait();
-executor.run_until(taskflow, [cnt=0] () mutable { return ++cnt == 10; });
-@endcode
-
-All the @c run methods are @em thread-safe. You can submit multiple
-taskflows at the same time to an executor from different threads.
-*/
-class Executor {
-
-  friend class FlowBuilder;
-  friend class Subflow;
-  friend class Runtime;
-
-  public:
-
-    /**
-    @brief constructs the executor with @c N worker threads
-
-    The constructor spawns @c N worker threads to run tasks in a
-    work-stealing loop. The number of workers must be greater than zero
-    or an exception will be thrown.
-    By default, the number of worker threads is equal to the maximum
-    hardware concurrency returned by std::thread::hardware_concurrency.
-    */
-    explicit Executor(size_t N = std::thread::hardware_concurrency());
-
-    /**
-    @brief destructs the executor
-
-    The destructor calls Executor::wait_for_all to wait for all submitted
-    taskflows to complete and then notifies all worker threads to stop
-    and join these threads.
-    */
-    ~Executor();
-
-    /**
-    @brief runs a taskflow once
-
-    @param taskflow a tf::Taskflow object
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes the given taskflow once and returns a tf::Future
-    object that eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(taskflow);
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-
-    @attention
-    The executor does not own the given taskflow. It is your responsibility to
-    ensure the taskflow remains alive during its execution.
-    */
-    tf::Future<void> run(Taskflow& taskflow);
-
-    /**
-    @brief runs a moved taskflow once
-
-    @param taskflow a moved tf::Taskflow object
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes a moved taskflow once and returns a tf::Future
-    object that eventually holds the result of the execution.
-    The executor will take care of the lifetime of the moved taskflow.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(std::move(taskflow));
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-    */
-    tf::Future<void> run(Taskflow&& taskflow);
-
-    /**
-    @brief runs a taskflow once and invoke a callback upon completion
-
-    @param taskflow a tf::Taskflow object
-    @param callable a callable object to be invoked after this run
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes the given taskflow once and invokes the given
-    callable when the execution completes.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(taskflow, [](){ std::cout << "done"; });
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-
-    @attention
-    The executor does not own the given taskflow. It is your responsibility to
-    ensure the taskflow remains alive during its execution.
-    */
-    template<typename C>
-    tf::Future<void> run(Taskflow& taskflow, C&& callable);
-
-    /**
-    @brief runs a moved taskflow once and invoke a callback upon completion
-
-    @param taskflow a moved tf::Taskflow object
-    @param callable a callable object to be invoked after this run
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes a moved taskflow once and invokes the given
-    callable when the execution completes.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-    The executor will take care of the lifetime of the moved taskflow.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      std::move(taskflow), [](){ std::cout << "done"; }
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template<typename C>
-    tf::Future<void> run(Taskflow&& taskflow, C&& callable);
-
-    /**
-    @brief runs a taskflow for @c N times
-
-    @param taskflow a tf::Taskflow object
-    @param N number of runs
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes the given taskflow @c N times and returns a tf::Future
-    object that eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run_n(taskflow, 2);  // run taskflow 2 times
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-
-    @attention
-    The executor does not own the given taskflow. It is your responsibility to
-    ensure the taskflow remains alive during its execution.
-    */
-    tf::Future<void> run_n(Taskflow& taskflow, size_t N);
-
-    /**
-    @brief runs a moved taskflow for @c N times
-
-    @param taskflow a moved tf::Taskflow object
-    @param N number of runs
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes a moved taskflow @c N times and returns a tf::Future
-    object that eventually holds the result of the execution.
-    The executor will take care of the lifetime of the moved taskflow.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run_n(
-      std::move(taskflow), 2    // run the moved taskflow 2 times
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-    */
-    tf::Future<void> run_n(Taskflow&& taskflow, size_t N);
-
-    /**
-    @brief runs a taskflow for @c N times and then invokes a callback
-
-    @param taskflow a tf::Taskflow
-    @param N number of runs
-    @param callable a callable object to be invoked after this run
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes the given taskflow @c N times and invokes the given
-    callable when the execution completes.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      taskflow, 2, [](){ std::cout << "done"; }  // runs taskflow 2 times and invoke
-                                                 // the lambda to print "done"
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-
-    @attention
-    The executor does not own the given taskflow. It is your responsibility to
-    ensure the taskflow remains alive during its execution.
-    */
-    template<typename C>
-    tf::Future<void> run_n(Taskflow& taskflow, size_t N, C&& callable);
-
-    /**
-    @brief runs a moved taskflow for @c N times and then invokes a callback
-
-    @param taskflow a moved tf::Taskflow
-    @param N number of runs
-    @param callable a callable object to be invoked after this run
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes a moved taskflow @c N times and invokes the given
-    callable when the execution completes.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      // run the moved taskflow 2 times and invoke the lambda to print "done"
-      std::move(taskflow), 2, [](){ std::cout << "done"; }
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template<typename C>
-    tf::Future<void> run_n(Taskflow&& taskflow, size_t N, C&& callable);
-
-    /**
-    @brief runs a taskflow multiple times until the predicate becomes true
-
-    @param taskflow a tf::Taskflow
-    @param pred a boolean predicate to return @c true for stop
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes the given taskflow multiple times until
-    the predicate returns @c true.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      taskflow, [](){ return rand()%10 == 0 }
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-
-    @attention
-    The executor does not own the given taskflow. It is your responsibility to
-    ensure the taskflow remains alive during its execution.
-    */
-    template<typename P>
-    tf::Future<void> run_until(Taskflow& taskflow, P&& pred);
-
-    /**
-    @brief runs a moved taskflow and keeps running it
-           until the predicate becomes true
-
-    @param taskflow a moved tf::Taskflow object
-    @param pred a boolean predicate to return @c true for stop
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes a moved taskflow multiple times until
-    the predicate returns @c true.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-    The executor will take care of the lifetime of the moved taskflow.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      std::move(taskflow), [](){ return rand()%10 == 0 }
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template<typename P>
-    tf::Future<void> run_until(Taskflow&& taskflow, P&& pred);
-
-    /**
-    @brief runs a taskflow multiple times until the predicate becomes true and
-           then invokes the callback
-
-    @param taskflow a tf::Taskflow
-    @param pred a boolean predicate to return @c true for stop
-    @param callable a callable object to be invoked after this run completes
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes the given taskflow multiple times until
-    the predicate returns @c true and then invokes the given callable when
-    the execution completes.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      taskflow, [](){ return rand()%10 == 0 }, [](){ std::cout << "done"; }
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-
-    @attention
-    The executor does not own the given taskflow. It is your responsibility to
-    ensure the taskflow remains alive during its execution.
-    */
-    template<typename P, typename C>
-    tf::Future<void> run_until(Taskflow& taskflow, P&& pred, C&& callable);
-
-    /**
-    @brief runs a moved taskflow and keeps running
-           it until the predicate becomes true and then invokes the callback
-
-    @param taskflow a moved tf::Taskflow
-    @param pred a boolean predicate to return @c true for stop
-    @param callable a callable object to be invoked after this run completes
-
-    @return a tf::Future that holds the result of the execution
-
-    This member function executes a moved taskflow multiple times until
-    the predicate returns @c true and then invokes the given callable when
-    the execution completes.
-    This member function returns a tf::Future object that
-    eventually holds the result of the execution.
-    The executor will take care of the lifetime of the moved taskflow.
-
-    @code{.cpp}
-    tf::Future<void> future = executor.run(
-      std::move(taskflow),
-      [](){ return rand()%10 == 0 }, [](){ std::cout << "done"; }
-    );
-    // do something else
-    future.wait();
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template<typename P, typename C>
-    tf::Future<void> run_until(Taskflow&& taskflow, P&& pred, C&& callable);
-
-    /**
-    @brief wait for all tasks to complete
-
-    This member function waits until all submitted tasks
-    (e.g., taskflows, asynchronous tasks) to finish.
-
-    @code{.cpp}
-    executor.run(taskflow1);
-    executor.run_n(taskflow2, 10);
-    executor.run_n(taskflow3, 100);
-    executor.wait_for_all();  // wait until the above submitted taskflows finish
-    @endcode
-    */
-    void wait_for_all();
-
-    /**
-    @brief queries the number of worker threads
-
-    Each worker represents one unique thread spawned by an executor
-    upon its construction time.
-
-    @code{.cpp}
-    tf::Executor executor(4);
-    std::cout << executor.num_workers();    // 4
-    @endcode
-    */
-    size_t num_workers() const noexcept;
-
-    /**
-    @brief queries the number of running topologies at the time of this call
-
-    When a taskflow is submitted to an executor, a topology is created to store
-    runtime metadata of the running taskflow.
-    When the execution of the submitted taskflow finishes,
-    its corresponding topology will be removed from the executor.
-
-    @code{.cpp}
-    executor.run(taskflow);
-    std::cout << executor.num_topologies();  // 0 or 1 (taskflow still running)
-    @endcode
-    */
-    size_t num_topologies() const;
-
-    /**
-    @brief queries the number of running taskflows with moved ownership
-
-    @code{.cpp}
-    executor.run(std::move(taskflow));
-    std::cout << executor.num_taskflows();  // 0 or 1 (taskflow still running)
-    @endcode
-    */
-    size_t num_taskflows() const;
-
-    /**
-    @brief queries the id of the caller thread in this executor
-
-    Each worker has an unique id in the range of @c 0 to @c N-1 associated with
-    its parent executor.
-    If the caller thread does not belong to the executor, @c -1 is returned.
-
-    @code{.cpp}
-    tf::Executor executor(4);   // 4 workers in the executor
-    executor.this_worker_id();  // -1 (main thread is not a worker)
-
-    taskflow.emplace([&](){
-      std::cout << executor.this_worker_id();  // 0, 1, 2, or 3
-    });
-    executor.run(taskflow);
-    @endcode
-    */
-    int this_worker_id() const;
-
-    /**
-    @brief runs a given function asynchronously
-
-    @tparam F callable type
-    @tparam ArgsT parameter types
-
-    @param f callable object to call
-    @param args parameters to pass to the callable
-
-    @return a tf::Future that will holds the result of the execution
-
-    The method creates an asynchronous task to launch the given
-    function on the given arguments.
-    Unlike std::async, the return here is a @em tf::Future that holds
-    an optional object to the result.
-    If the asynchronous task is cancelled before it runs, the return is
-    a @c std::nullopt, or the value returned by the callable.
-
-    @code{.cpp}
-    tf::Future<std::optional<int>> future = executor.async([](){
-      std::cout << "create an asynchronous task and returns 1\n";
-      return 1;
-    });
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template <typename F, typename... ArgsT>
-    auto async(F&& f, ArgsT&&... args);
-
-    /**
-    @brief runs a given function asynchronously and gives a name to this task
-
-    @tparam F callable type
-    @tparam ArgsT parameter types
-
-    @param name name of the asynchronous task
-    @param f callable object to call
-    @param args parameters to pass to the callable
-
-    @return a tf::Future that will holds the result of the execution
-
-    The method creates a named asynchronous task to launch the given
-    function on the given arguments.
-    Naming an asynchronous task is primarily used for profiling and visualizing
-    the task execution timeline.
-    Unlike std::async, the return here is a tf::Future that holds
-    an optional object to the result.
-    If the asynchronous task is cancelled before it runs, the return is
-    a @c std::nullopt, or the value returned by the callable.
-
-    @code{.cpp}
-    tf::Future<std::optional<int>> future = executor.named_async("name", [](){
-      std::cout << "create an asynchronous task with a name and returns 1\n";
-      return 1;
-    });
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template <typename F, typename... ArgsT>
-    auto named_async(const std::string& name, F&& f, ArgsT&&... args);
-
-    /**
-    @brief similar to tf::Executor::async but does not return a future object
-
-    This member function is more efficient than tf::Executor::async
-    and is encouraged to use when there is no data returned.
-
-    @code{.cpp}
-    executor.silent_async([](){
-      std::cout << "create an asynchronous task with no return\n";
-    });
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template <typename F, typename... ArgsT>
-    void silent_async(F&& f, ArgsT&&... args);
-
-    /**
-    @brief similar to tf::Executor::named_async but does not return a future object
-
-    This member function is more efficient than tf::Executor::named_async
-    and is encouraged to use when there is no data returned.
-
-    @code{.cpp}
-    executor.named_silent_async("name", [](){
-      std::cout << "create an asynchronous task with a name and no return\n";
-    });
-    @endcode
-
-    This member function is thread-safe.
-    */
-    template <typename F, typename... ArgsT>
-    void named_silent_async(const std::string& name, F&& f, ArgsT&&... args);
-
-    /**
-    @brief constructs an observer to inspect the activities of worker threads
-
-    @tparam Observer observer type derived from tf::ObserverInterface
-    @tparam ArgsT argument parameter pack
-
-    @param args arguments to forward to the constructor of the observer
-
-    @return a shared pointer to the created observer
-
-    Each executor manages a list of observers with shared ownership with callers.
-    For each of these observers, the two member functions,
-    tf::ObserverInterface::on_entry and tf::ObserverInterface::on_exit
-    will be called before and after the execution of a task.
-
-    This member function is not thread-safe.
-    */
-    template <typename Observer, typename... ArgsT>
-    std::shared_ptr<Observer> make_observer(ArgsT&&... args);
-
-    /**
-    @brief removes an observer from the executor
-
-    This member function is not thread-safe.
-    */
-    template <typename Observer>
-    void remove_observer(std::shared_ptr<Observer> observer);
-
-    /**
-    @brief queries the number of observers
-    */
-    size_t num_observers() const noexcept;
-
-  private:
-
-    std::condition_variable _topology_cv;
-    std::mutex _taskflow_mutex;
-    std::mutex _topology_mutex;
-    std::mutex _wsq_mutex;
-
-    size_t _num_topologies {0};
-
-    std::unordered_map<std::thread::id, size_t> _wids;
-    std::vector<Worker> _workers;
-    std::vector<std::thread> _threads;
-    std::list<Taskflow> _taskflows;
-
-    Notifier _notifier;
-
-    TaskQueue<Node*> _wsq;
-
-    std::atomic<size_t> _num_actives {0};
-    std::atomic<size_t> _num_thieves {0};
-    std::atomic<bool>   _done {0};
-
-    std::unordered_set<std::shared_ptr<ObserverInterface>> _observers;
-
-    Worker* _this_worker();
-
-    bool _wait_for_task(Worker&, Node*&);
-
-    void _observer_prologue(Worker&, Node*);
-    void _observer_epilogue(Worker&, Node*);
-    void _spawn(size_t);
-    void _worker_loop(Worker&);
-    void _exploit_task(Worker&, Node*&);
-    void _explore_task(Worker&, Node*&);
-    void _consume_task(Worker&, Node*);
-    void _schedule(Worker&, Node*);
-    void _schedule(Node*);
-    void _schedule(Worker&, const SmallVector<Node*>&);
-    void _schedule(const SmallVector<Node*>&);
-    void _set_up_topology(Worker*, Topology*);
-    void _tear_down_topology(Worker&, Topology*);
-    void _tear_down_async(Node*);
-    void _tear_down_invoke(Worker&, Node*);
-    void _cancel_invoke(Worker&, Node*);
-    void _increment_topology();
-    void _decrement_topology();
-    void _decrement_topology_and_notify();
-    void _invoke(Worker&, Node*);
-    void _invoke_static_task(Worker&, Node*);
-    void _invoke_dynamic_task(Worker&, Node*);
-    void _invoke_dynamic_task_external(Worker&, Node*, Graph&, bool);
-    void _invoke_dynamic_task_internal(Worker&, Node*, Graph&);
-    void _invoke_condition_task(Worker&, Node*, SmallVector<int>&);
-    void _invoke_multi_condition_task(Worker&, Node*, SmallVector<int>&);
-    void _invoke_module_task(Worker&, Node*, bool&);
-    void _invoke_module_task_internal(Worker&, Node*, Graph&, bool&);
-    void _invoke_async_task(Worker&, Node*);
-    void _invoke_silent_async_task(Worker&, Node*);
-    void _invoke_cudaflow_task(Worker&, Node*);
-    void _invoke_syclflow_task(Worker&, Node*);
-    void _invoke_runtime_task(Worker&, Node*);
-
-    template <typename C,
-      std::enable_if_t<is_cudaflow_task_v<C>, void>* = nullptr
-    >
-    void _invoke_cudaflow_task_entry(Node*, C&&);
-
-    template <typename C, typename Q,
-      std::enable_if_t<is_syclflow_task_v<C>, void>* = nullptr
-    >
-    void _invoke_syclflow_task_entry(Node*, C&&, Q&);
-};
-
-// Constructor
-inline Executor::Executor(size_t N) :
-  _workers    {N},
-  _notifier   {N} {
-
-  if(N == 0) {
-    TF_THROW("no cpu workers to execute taskflows");
-  }
-
-  _spawn(N);
-
-  // instantite the default observer if requested
-  if(has_env(TF_ENABLE_PROFILER)) {
-    TFProfManager::get()._manage(make_observer<TFProfObserver>());
-  }
-}
-
-// Destructor
-inline Executor::~Executor() {
-
-  // wait for all topologies to complete
-  wait_for_all();
-
-  // shut down the scheduler
-  _done = true;
-
-  _notifier.notify(true);
-
-  for(auto& t : _threads){
-    t.join();
-  }
-}
-
-// Function: num_workers
-inline size_t Executor::num_workers() const noexcept {
-  return _workers.size();
-}
-
-// Function: num_topologies
-inline size_t Executor::num_topologies() const {
-  return _num_topologies;
-}
-
-// Function: num_taskflows
-inline size_t Executor::num_taskflows() const {
-  return _taskflows.size();
-}
-
-// Function: _this_worker
-inline Worker* Executor::_this_worker() {
-  auto itr = _wids.find(std::this_thread::get_id());
-  return itr == _wids.end() ? nullptr : &_workers[itr->second];
-}
-
-// Function: named_async
-template <typename F, typename... ArgsT>
-auto Executor::named_async(const std::string& name, F&& f, ArgsT&&... args) {
-
-  _increment_topology();
-
-  using T = std::invoke_result_t<F, ArgsT...>;
-  using R = std::conditional_t<std::is_same_v<T, void>, void, std::optional<T>>;
-
-  std::promise<R> p;
-
-  auto tpg = std::make_shared<AsyncTopology>();
-
-  Future<R> fu(p.get_future(), tpg);
-
-  auto node = node_pool.animate(
-    std::in_place_type_t<Node::Async>{},
-    [p=make_moc(std::move(p)), f=std::forward<F>(f), args...]
-    (bool cancel) mutable {
-      if constexpr(std::is_same_v<R, void>) {
-        if(!cancel) {
-          f(args...);
-        }
-        p.object.set_value();
-      }
-      else {
-        p.object.set_value(cancel ? std::nullopt : std::make_optional(f(args...)));
-      }
-    },
-    std::move(tpg)
-  );
-
-  node->_name = name;
-
-  if(auto w = _this_worker(); w) {
-    _schedule(*w, node);
-  }
-  else{
-    _schedule(node);
-  }
-
-  return fu;
-}
-
-// Function: async
-template <typename F, typename... ArgsT>
-auto Executor::async(F&& f, ArgsT&&... args) {
-  return named_async("", std::forward<F>(f), std::forward<ArgsT>(args)...);
-}
-
-// Function: named_silent_async
-template <typename F, typename... ArgsT>
-void Executor::named_silent_async(
-  const std::string& name, F&& f, ArgsT&&... args
-) {
-
-  _increment_topology();
-
-  Node* node = node_pool.animate(
-    std::in_place_type_t<Node::SilentAsync>{},
-    [f=std::forward<F>(f), args...] () mutable {
-      f(args...);
-    }
-  );
-
-  node->_name = name;
-
-  if(auto w = _this_worker(); w) {
-    _schedule(*w, node);
-  }
-  else {
-    _schedule(node);
-  }
-}
-
-// Function: silent_async
-template <typename F, typename... ArgsT>
-void Executor::silent_async(F&& f, ArgsT&&... args) {
-  named_silent_async("", std::forward<F>(f), std::forward<ArgsT>(args)...);
-}
-
-// Function: this_worker_id
-inline int Executor::this_worker_id() const {
-  auto i = _wids.find(std::this_thread::get_id());
-  return i == _wids.end() ? -1 : static_cast<int>(_workers[i->second]._id);
-}
-
-// Procedure: _spawn
-inline void Executor::_spawn(size_t N) {
-
-  std::mutex mutex;
-  std::condition_variable cond;
-  size_t n=0;
-
-  for(size_t id=0; id<N; ++id) {
-
-    _workers[id]._id = id;
-    _workers[id]._vtm = id;
-    _workers[id]._executor = this;
-    _workers[id]._waiter = &_notifier._waiters[id];
-
-    _threads.emplace_back([this] (
-      Worker& w, std::mutex& mutex, std::condition_variable& cond, size_t& n
-    ) -> void {
-
-      // enables the mapping
-      {
-        std::scoped_lock lock(mutex);
-        _wids[std::this_thread::get_id()] = w._id;
-        if(n++; n == num_workers()) {
-          cond.notify_one();
-        }
-      }
-
-      //this_worker().worker = &w;
-
-      Node* t = nullptr;
-
-      // must use 1 as condition instead of !done
-      while(1) {
-
-        // execute the tasks.
-        _exploit_task(w, t);
-
-        // wait for tasks
-        if(_wait_for_task(w, t) == false) {
-          break;
-        }
-      }
-
-    }, std::ref(_workers[id]), std::ref(mutex), std::ref(cond), std::ref(n));
-  }
-
-  std::unique_lock<std::mutex> lock(mutex);
-  cond.wait(lock, [&](){ return n==N; });
-}
-
-// Function: _consume_task
-inline void Executor::_consume_task(Worker& w, Node* p) {
-
-  std::uniform_int_distribution<size_t> rdvtm(0, _workers.size()-1);
-
-  while(p->_join_counter != 0) {
-    exploit:
-    if(auto t = w._wsq.pop(); t) {
-      _invoke(w, t);
-    }
-    else {
-      size_t num_steals = 0;
-      //size_t num_pauses = 0;
-      size_t max_steals = ((_workers.size() + 1) << 1);
-
-      explore:
-
-      t = (w._id == w._vtm) ? _wsq.steal() : _workers[w._vtm]._wsq.steal();
-      if(t) {
-        _invoke(w, t);
-        goto exploit;
-      }
-      else if(p->_join_counter != 0){
-
-        if(num_steals++ > max_steals) {
-          std::this_thread::yield();
-        }
-
-        //std::this_thread::yield();
-        w._vtm = rdvtm(w._rdgen);
-        goto explore;
-      }
-      else {
-        break;
-      }
-    }
-  }
-}
-
-// Function: _explore_task
-inline void Executor::_explore_task(Worker& w, Node*& t) {
-
-  //assert(_workers[w].wsq.empty());
-  //assert(!t);
-
-  size_t num_steals = 0;
-  size_t num_yields = 0;
-  size_t max_steals = ((_workers.size() + 1) << 1);
-
-  std::uniform_int_distribution<size_t> rdvtm(0, _workers.size()-1);
-
-  do {
-    t = (w._id == w._vtm) ? _wsq.steal() : _workers[w._vtm]._wsq.steal();
-
-    if(t) {
-      break;
-    }
-
-    if(num_steals++ > max_steals) {
-      std::this_thread::yield();
-      if(num_yields++ > 100) {
-        break;
-      }
-    }
-
-    w._vtm = rdvtm(w._rdgen);
-  } while(!_done);
-
-}
-
-// Procedure: _exploit_task
-inline void Executor::_exploit_task(Worker& w, Node*& t) {
-
-  if(t) {
-
-    if(_num_actives.fetch_add(1) == 0 && _num_thieves == 0) {
-      _notifier.notify(false);
-    }
-
-    while(t) {
-      _invoke(w, t);
-      t = w._wsq.pop();
-    }
-
-    --_num_actives;
-  }
-}
-
-// Function: _wait_for_task
-inline bool Executor::_wait_for_task(Worker& worker, Node*& t) {
-
-  wait_for_task:
-
-  //assert(!t);
-
-  ++_num_thieves;
-
-  explore_task:
-
-  _explore_task(worker, t);
-
-  if(t) {
-    if(_num_thieves.fetch_sub(1) == 1) {
-      _notifier.notify(false);
-    }
-    return true;
-  }
-
-  _notifier.prepare_wait(worker._waiter);
-
-  //if(auto vtm = _find_vtm(me); vtm != _workers.size()) {
-  if(!_wsq.empty()) {
-
-    _notifier.cancel_wait(worker._waiter);
-    //t = (vtm == me) ? _wsq.steal() : _workers[vtm].wsq.steal();
-
-    t = _wsq.steal();  // must steal here
-    if(t) {
-      if(_num_thieves.fetch_sub(1) == 1) {
-        _notifier.notify(false);
-      }
-      return true;
-    }
-    else {
-      worker._vtm = worker._id;
-      goto explore_task;
-    }
-  }
-
-  if(_done) {
-    _notifier.cancel_wait(worker._waiter);
-    _notifier.notify(true);
-    --_num_thieves;
-    return false;
-  }
-
-  if(_num_thieves.fetch_sub(1) == 1) {
-    if(_num_actives) {
-      _notifier.cancel_wait(worker._waiter);
-      goto wait_for_task;
-    }
-    // check all queues again
-    for(auto& w : _workers) {
-      if(!w._wsq.empty()) {
-        worker._vtm = w._id;
-        _notifier.cancel_wait(worker._waiter);
-        goto wait_for_task;
-      }
-    }
-  }
-
-  // Now I really need to relinguish my self to others
-  _notifier.commit_wait(worker._waiter);
-
-  return true;
-}
-
-// Function: make_observer
-template<typename Observer, typename... ArgsT>
-std::shared_ptr<Observer> Executor::make_observer(ArgsT&&... args) {
-
-  static_assert(
-    std::is_base_of_v<ObserverInterface, Observer>,
-    "Observer must be derived from ObserverInterface"
-  );
-
-  // use a local variable to mimic the constructor
-  auto ptr = std::make_shared<Observer>(std::forward<ArgsT>(args)...);
-
-  ptr->set_up(_workers.size());
-
-  _observers.emplace(std::static_pointer_cast<ObserverInterface>(ptr));
-
-  return ptr;
-}
-
-// Procedure: remove_observer
-template <typename Observer>
-void Executor::remove_observer(std::shared_ptr<Observer> ptr) {
-
-  static_assert(
-    std::is_base_of_v<ObserverInterface, Observer>,
-    "Observer must be derived from ObserverInterface"
-  );
-
-  _observers.erase(std::static_pointer_cast<ObserverInterface>(ptr));
-}
-
-// Function: num_observers
-inline size_t Executor::num_observers() const noexcept {
-  return _observers.size();
-}
-
-// Procedure: _schedule
-inline void Executor::_schedule(Worker& worker, Node* node) {
-
-  node->_state.fetch_or(Node::READY, std::memory_order_release);
-
-  // caller is a worker to this pool
-  if(worker._executor == this) {
-    worker._wsq.push(node);
-    return;
-  }
-
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    _wsq.push(node);
-  }
-
-  _notifier.notify(false);
-}
-
-// Procedure: _schedule
-inline void Executor::_schedule(Node* node) {
-
-  node->_state.fetch_or(Node::READY, std::memory_order_release);
-
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    _wsq.push(node);
-  }
-
-  _notifier.notify(false);
-}
-
-// Procedure: _schedule
-inline void Executor::_schedule(
-  Worker& worker, const SmallVector<Node*>& nodes
-) {
-
-  // We need to cacth the node count to avoid accessing the nodes
-  // vector while the parent topology is removed!
-  const auto num_nodes = nodes.size();
-
-  if(num_nodes == 0) {
-    return;
-  }
-
-  // make the node ready
-  for(size_t i=0; i<num_nodes; ++i) {
-    nodes[i]->_state.fetch_or(Node::READY, std::memory_order_release);
-  }
-
-  if(worker._executor == this) {
-    for(size_t i=0; i<num_nodes; ++i) {
-      worker._wsq.push(nodes[i]);
-    }
-    return;
-  }
-
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    for(size_t k=0; k<num_nodes; ++k) {
-      _wsq.push(nodes[k]);
-    }
-  }
-
-  _notifier.notify_n(num_nodes);
-}
-
-// Procedure: _schedule
-inline void Executor::_schedule(const SmallVector<Node*>& nodes) {
-
-  // parent topology may be removed!
-  const auto num_nodes = nodes.size();
-
-  if(num_nodes == 0) {
-    return;
-  }
-
-  // make the node ready
-  for(size_t i=0; i<num_nodes; ++i) {
-    nodes[i]->_state.fetch_or(Node::READY, std::memory_order_release);
-  }
-
-  {
-    std::lock_guard<std::mutex> lock(_wsq_mutex);
-    for(size_t k=0; k<num_nodes; ++k) {
-      _wsq.push(nodes[k]);
-    }
-  }
-
-  _notifier.notify_n(num_nodes);
-}
-
-// Procedure: _invoke
-inline void Executor::_invoke(Worker& worker, Node* node) {
-
-  int state;
-  SmallVector<int> conds;
-
-  // synchronize all outstanding memory operations caused by reordering
-  do {
-    state = node->_state.load(std::memory_order_acquire);
-  } while(! (state & Node::READY));
-
-  // unwind stack for deferred node
-  if(state & Node::DEFERRED) {
-    node->_state.fetch_and(~Node::DEFERRED, std::memory_order_relaxed);
-    goto invoke_epilogue;
-  }
-
-  //while(!(node->_state.load(std::memory_order_acquire) & Node::READY));
-
-  invoke_prologue:
-
-  // no need to do other things if the topology is cancelled
-  if(node->_is_cancelled()) {
-    _cancel_invoke(worker, node);
-    return;
-  }
-
-  // if acquiring semaphore(s) exists, acquire them first
-  if(node->_semaphores && !node->_semaphores->to_acquire.empty()) {
-    SmallVector<Node*> nodes;
-    if(!node->_acquire_all(nodes)) {
-      _schedule(worker, nodes);
-      return;
-    }
-    node->_state.fetch_or(Node::ACQUIRED, std::memory_order_release);
-  }
-
-  // condition task
-  //int cond = -1;
-  //SmallVector<int> conds = { -1 };
-
-  // switch is faster than nested if-else due to jump table
-  switch(node->_handle.index()) {
-    // static task
-    case Node::STATIC:{
-      _invoke_static_task(worker, node);
-    }
-    break;
-
-    // dynamic task
-    case Node::DYNAMIC: {
-      _invoke_dynamic_task(worker, node);
-    }
-    break;
-
-    // condition task
-    case Node::CONDITION: {
-      _invoke_condition_task(worker, node, conds);
-    }
-    break;
-
-    // multi-condition task
-    case Node::MULTI_CONDITION: {
-      _invoke_multi_condition_task(worker, node, conds);
-    }
-    break;
-
-    // module task
-    case Node::MODULE: {
-      bool deferred = false;
-      _invoke_module_task(worker, node, deferred);
-      if(deferred) {
-        return;
-      }
-    }
-    break;
-
-    // async task
-    case Node::ASYNC: {
-      _invoke_async_task(worker, node);
-      _tear_down_async(node);
-      return ;
-    }
-    break;
-
-    // silent async task
-    case Node::SILENT_ASYNC: {
-      _invoke_silent_async_task(worker, node);
-      _tear_down_async(node);
-      return ;
-    }
-    break;
-
-    // cudaflow task
-    case Node::CUDAFLOW: {
-      _invoke_cudaflow_task(worker, node);
-    }
-    break;
-
-    // syclflow task
-    case Node::SYCLFLOW: {
-      _invoke_syclflow_task(worker, node);
-    }
-    break;
-
-    // runtime task
-    case Node::RUNTIME: {
-      _invoke_runtime_task(worker, node);
-    }
-    break;
-
-    // monostate (placeholder)
-    default:
-    break;
-  }
-
-  invoke_epilogue:
-
-  // if releasing semaphores exist, release them
-  if(node->_semaphores && !node->_semaphores->to_release.empty()) {
-    _schedule(worker, node->_release_all());
-  }
-
-  // We MUST recover the dependency since the graph may have cycles.
-  // This must be done before scheduling the successors, otherwise this might cause
-  // race condition on the _dependents
-  if((node->_state.load(std::memory_order_relaxed) & Node::CONDITIONED)) {
-    node->_join_counter = node->num_strong_dependents();
-  }
-  else {
-    node->_join_counter = node->num_dependents();
-  }
-
-  // acquire the parent flow counter
-  auto& j = (node->_parent) ? node->_parent->_join_counter :
-                              node->_topology->_join_counter;
-
-  Node* cache {nullptr};
-
-  // At this point, the node storage might be destructed (to be verified)
-  // case 1: non-condition task
-  switch(node->_handle.index()) {
-
-    // condition and multi-condition tasks
-    case Node::CONDITION:
-    case Node::MULTI_CONDITION: {
-      for(auto cond : conds) {
-        if(cond >= 0 && static_cast<size_t>(cond) < node->_successors.size()) {
-          auto s = node->_successors[cond];
-          // zeroing the join counter for invariant
-          s->_join_counter.store(0, std::memory_order_relaxed);
-          j.fetch_add(1);
-          if(cache) {
-            _schedule(worker, cache);
-          }
-          cache = s;
-        }
-      }
-    }
-    break;
-
-    // non-condition task
-    default: {
-      for(size_t i=0; i<node->_successors.size(); ++i) {
-        if(--(node->_successors[i]->_join_counter) == 0) {
-          j.fetch_add(1);
-          if(cache) {
-            _schedule(worker, cache);
-          }
-          cache = node->_successors[i];
-        }
-      }
-    }
-    break;
-  }
-
-  // tear_down the invoke
-  _tear_down_invoke(worker, node);
-
-  // perform tail recursion elimination for the right-most child to reduce
-  // the number of expensive pop/push operations through the task queue
-  if(cache) {
-    node = cache;
-    //node->_state.fetch_or(Node::READY, std::memory_order_release);
-    goto invoke_prologue;
-  }
-}
-
-// Procedure: _tear_down_async
-inline void Executor::_tear_down_async(Node* node) {
-  if(node->_parent) {
-    node->_parent->_join_counter.fetch_sub(1);
-  }
-  else {
-    _decrement_topology_and_notify();
-  }
-  node_pool.recycle(node);
-}
-
-// Proecdure: _tear_down_invoke
-inline void Executor::_tear_down_invoke(Worker& worker, Node* node) {
-  // we must check parent first before substracting the join counter,
-  // or it can introduce data race
-  if(auto parent = node->_parent; parent == nullptr) {
-    if(node->_topology->_join_counter.fetch_sub(1) == 1) {
-      _tear_down_topology(worker, node->_topology);
-    }
-  }
-  else {
-    // prefetch the deferred status, as subtracting the join counter can
-    // immediately cause the other worker to release the subflow
-    auto deferred = parent->_state.load(std::memory_order_relaxed) & Node::DEFERRED;
-    if(parent->_join_counter.fetch_sub(1) == 1 && deferred) {
-      _schedule(worker, parent);
-    }
-  }
-}
-
-// Procedure: _cancel_invoke
-inline void Executor::_cancel_invoke(Worker& worker, Node* node) {
-
-  switch(node->_handle.index()) {
-    // async task needs to carry out the promise
-    case Node::ASYNC:
-      std::get_if<Node::Async>(&(node->_handle))->work(true);
-      _tear_down_async(node);
-    break;
-
-    // silent async doesn't need to carry out the promise
-    case Node::SILENT_ASYNC:
-      _tear_down_async(node);
-    break;
-
-    // tear down topology if the node is the last leaf
-    default: {
-      _tear_down_invoke(worker, node);
-    }
-    break;
-  }
-}
-
-// Procedure: _observer_prologue
-inline void Executor::_observer_prologue(Worker& worker, Node* node) {
-  for(auto& observer : _observers) {
-    observer->on_entry(WorkerView(worker), TaskView(*node));
-  }
-}
-
-// Procedure: _observer_epilogue
-inline void Executor::_observer_epilogue(Worker& worker, Node* node) {
-  for(auto& observer : _observers) {
-    observer->on_exit(WorkerView(worker), TaskView(*node));
-  }
-}
-
-// Procedure: _invoke_static_task
-inline void Executor::_invoke_static_task(Worker& worker, Node* node) {
-  _observer_prologue(worker, node);
-  std::get_if<Node::Static>(&node->_handle)->work();
-  _observer_epilogue(worker, node);
-}
-
-// Procedure: _invoke_dynamic_task
-inline void Executor::_invoke_dynamic_task(Worker& w, Node* node) {
-
-  _observer_prologue(w, node);
-
-  auto handle = std::get_if<Node::Dynamic>(&node->_handle);
-
-  handle->subgraph._clear();
-
-  Subflow sf(*this, w, node, handle->subgraph);
-
-  handle->work(sf);
-
-  if(sf._joinable) {
-    _invoke_dynamic_task_internal(w, node, handle->subgraph);
-  }
-
-  _observer_epilogue(w, node);
-}
-
-// Procedure: _invoke_dynamic_task_external
-inline void Executor::_invoke_dynamic_task_external(
-  Worker& w, Node* p, Graph& g, bool detach
-) {
-
-  // graph is empty and has no async tasks
-  if(g.empty() && p->_join_counter == 0) {
-    return;
-  }
-
-  SmallVector<Node*> src;
-
-  for(auto n : g._nodes) {
-
-    n->_topology = p->_topology;
-    n->_state.store(0, std::memory_order_relaxed);
-    n->_set_up_join_counter();
-
-    if(detach) {
-      n->_parent = nullptr;
-      n->_state.fetch_or(Node::DETACHED, std::memory_order_relaxed);
-    }
-    else {
-      n->_parent = p;
-    }
-
-    if(n->num_dependents() == 0) {
-      src.push_back(n);
-    }
-  }
-
-  // detach here
-  if(detach) {
-
-    {
-      std::lock_guard<std::mutex> lock(p->_topology->_taskflow._mutex);
-      p->_topology->_taskflow._graph._merge(std::move(g));
-    }
-
-    p->_topology->_join_counter.fetch_add(src.size());
-    _schedule(w, src);
-  }
-  // join here
-  else {
-    p->_join_counter.fetch_add(src.size());
-    _schedule(w, src);
-    _consume_task(w, p);
-  }
-}
-
-// Procedure: _invoke_dynamic_task_internal
-inline void Executor::_invoke_dynamic_task_internal(
-  Worker& w, Node* p, Graph& g
-) {
-
-  // graph is empty and has no async tasks
-  if(g.empty() && p->_join_counter == 0) {
-    return;
-  }
-
-  SmallVector<Node*> src;
-
-  for(auto n : g._nodes) {
-    n->_topology = p->_topology;
-    n->_state.store(0, std::memory_order_relaxed);
-    n->_set_up_join_counter();
-    n->_parent = p;
-    if(n->num_dependents() == 0) {
-      src.push_back(n);
-    }
-  }
-  p->_join_counter.fetch_add(src.size());
-  _schedule(w, src);
-  _consume_task(w, p);
-}
-
-// Procedure: _invoke_module_task_internal
-inline void Executor::_invoke_module_task_internal(
-  Worker& w, Node* p, Graph& g, bool& deferred
-) {
-
-  // graph is empty and has no async tasks
-  if(g.empty()) {
-    return;
-  }
-
-  // set deferred
-  deferred = true;
-  p->_state.fetch_or(Node::DEFERRED, std::memory_order_relaxed);
-
-  SmallVector<Node*> src;
-
-  for(auto n : g._nodes) {
-    n->_topology = p->_topology;
-    n->_state.store(0, std::memory_order_relaxed);
-    n->_set_up_join_counter();
-    n->_parent = p;
-    if(n->num_dependents() == 0) {
-      src.push_back(n);
-    }
-  }
-  p->_join_counter.fetch_add(src.size());
-  _schedule(w, src);
-}
-
-// Procedure: _invoke_condition_task
-inline void Executor::_invoke_condition_task(
-  Worker& worker, Node* node, SmallVector<int>& conds
-) {
-  _observer_prologue(worker, node);
-  conds = { std::get_if<Node::Condition>(&node->_handle)->work() };
-  _observer_epilogue(worker, node);
-}
-
-// Procedure: _invoke_multi_condition_task
-inline void Executor::_invoke_multi_condition_task(
-  Worker& worker, Node* node, SmallVector<int>& conds
-) {
-  _observer_prologue(worker, node);
-  conds = std::get_if<Node::MultiCondition>(&node->_handle)->work();
-  _observer_epilogue(worker, node);
-}
-
-// Procedure: _invoke_cudaflow_task
-inline void Executor::_invoke_cudaflow_task(Worker& worker, Node* node) {
-  _observer_prologue(worker, node);
-  std::get_if<Node::cudaFlow>(&node->_handle)->work(*this, node);
-  _observer_epilogue(worker, node);
-}
-
-// Procedure: _invoke_syclflow_task
-inline void Executor::_invoke_syclflow_task(Worker& worker, Node* node) {
-  _observer_prologue(worker, node);
-  std::get_if<Node::syclFlow>(&node->_handle)->work(*this, node);
-  _observer_epilogue(worker, node);
-}
-
-// Procedure: _invoke_module_task
-inline void Executor::_invoke_module_task(Worker& w, Node* node, bool& deferred) {
-  _observer_prologue(w, node);
-  _invoke_module_task_internal(
-    w, node, std::get_if<Node::Module>(&node->_handle)->graph, deferred
-  );
-  _observer_epilogue(w, node);
-}
-
-// Procedure: _invoke_async_task
-inline void Executor::_invoke_async_task(Worker& w, Node* node) {
-  _observer_prologue(w, node);
-  std::get_if<Node::Async>(&node->_handle)->work(false);
-  _observer_epilogue(w, node);
-}
-
-// Procedure: _invoke_silent_async_task
-inline void Executor::_invoke_silent_async_task(Worker& w, Node* node) {
-  _observer_prologue(w, node);
-  std::get_if<Node::SilentAsync>(&node->_handle)->work();
-  _observer_epilogue(w, node);
-}
-
-// Procedure: _invoke_runtime_task
-inline void Executor::_invoke_runtime_task(Worker& w, Node* node) {
-  _observer_prologue(w, node);
-  Runtime rt(*this, w, node);
-  std::get_if<Node::Runtime>(&node->_handle)->work(rt);
-  _observer_epilogue(w, node);
-}
-
-// Function: run
-inline tf::Future<void> Executor::run(Taskflow& f) {
-  return run_n(f, 1, [](){});
-}
-
-// Function: run
-inline tf::Future<void> Executor::run(Taskflow&& f) {
-  return run_n(std::move(f), 1, [](){});
-}
-
-// Function: run
-template <typename C>
-tf::Future<void> Executor::run(Taskflow& f, C&& c) {
-  return run_n(f, 1, std::forward<C>(c));
-}
-
-// Function: run
-template <typename C>
-tf::Future<void> Executor::run(Taskflow&& f, C&& c) {
-  return run_n(std::move(f), 1, std::forward<C>(c));
-}
-
-// Function: run_n
-inline tf::Future<void> Executor::run_n(Taskflow& f, size_t repeat) {
-  return run_n(f, repeat, [](){});
-}
-
-// Function: run_n
-inline tf::Future<void> Executor::run_n(Taskflow&& f, size_t repeat) {
-  return run_n(std::move(f), repeat, [](){});
-}
-
-// Function: run_n
-template <typename C>
-tf::Future<void> Executor::run_n(Taskflow& f, size_t repeat, C&& c) {
-  return run_until(
-    f, [repeat]() mutable { return repeat-- == 0; }, std::forward<C>(c)
-  );
-}
-
-// Function: run_n
-template <typename C>
-tf::Future<void> Executor::run_n(Taskflow&& f, size_t repeat, C&& c) {
-  return run_until(
-    std::move(f), [repeat]() mutable { return repeat-- == 0; }, std::forward<C>(c)
-  );
-}
-
-// Function: run_until
-template<typename P>
-tf::Future<void> Executor::run_until(Taskflow& f, P&& pred) {
-  return run_until(f, std::forward<P>(pred), [](){});
-}
-
-// Function: run_until
-template<typename P>
-tf::Future<void> Executor::run_until(Taskflow&& f, P&& pred) {
-  return run_until(std::move(f), std::forward<P>(pred), [](){});
-}
-
-// Function: run_until
-template <typename P, typename C>
-tf::Future<void> Executor::run_until(Taskflow& f, P&& p, C&& c) {
-
-  _increment_topology();
-
-  // Need to check the empty under the lock since dynamic task may
-  // define detached blocks that modify the taskflow at the same time
-  bool empty;
-  {
-    std::lock_guard<std::mutex> lock(f._mutex);
-    empty = f.empty();
-  }
-
-  // No need to create a real topology but returns an dummy future
-  if(empty || p()) {
-    c();
-    std::promise<void> promise;
-    promise.set_value();
-    _decrement_topology_and_notify();
-    return tf::Future<void>(promise.get_future(), std::monostate{});
-  }
-
-  // create a topology for this run
-  auto t = std::make_shared<Topology>(f, std::forward<P>(p), std::forward<C>(c));
-
-  // need to create future before the topology got torn down quickly
-  tf::Future<void> future(t->_promise.get_future(), t);
-
-  // modifying topology needs to be protected under the lock
-  {
-    std::lock_guard<std::mutex> lock(f._mutex);
-    f._topologies.push(t);
-    if(f._topologies.size() == 1) {
-      _set_up_topology(_this_worker(), t.get());
-    }
-  }
-
-  return future;
-}
-
-// Function: run_until
-template <typename P, typename C>
-tf::Future<void> Executor::run_until(Taskflow&& f, P&& pred, C&& c) {
-
-  std::list<Taskflow>::iterator itr;
-
-  {
-    std::scoped_lock<std::mutex> lock(_taskflow_mutex);
-    itr = _taskflows.emplace(_taskflows.end(), std::move(f));
-    itr->_satellite = itr;
-  }
-
-  return run_until(*itr, std::forward<P>(pred), std::forward<C>(c));
-}
-
-// Procedure: _increment_topology
-inline void Executor::_increment_topology() {
-  std::lock_guard<std::mutex> lock(_topology_mutex);
-  ++_num_topologies;
-}
-
-// Procedure: _decrement_topology_and_notify
-inline void Executor::_decrement_topology_and_notify() {
-  std::lock_guard<std::mutex> lock(_topology_mutex);
-  if(--_num_topologies == 0) {
-    _topology_cv.notify_all();
-  }
-}
-
-// Procedure: _decrement_topology
-inline void Executor::_decrement_topology() {
-  std::lock_guard<std::mutex> lock(_topology_mutex);
-  --_num_topologies;
-}
-
-// Procedure: wait_for_all
-inline void Executor::wait_for_all() {
-  std::unique_lock<std::mutex> lock(_topology_mutex);
-  _topology_cv.wait(lock, [&](){ return _num_topologies == 0; });
-}
-
-// Function: _set_up_topology
-inline void Executor::_set_up_topology(Worker* worker, Topology* tpg) {
-
-  // ---- under taskflow lock ----
-
-  tpg->_sources.clear();
-  tpg->_taskflow._graph._clear_detached();
-
-  // scan each node in the graph and build up the links
-  for(auto node : tpg->_taskflow._graph._nodes) {
-
-    node->_topology = tpg;
-    node->_state.store(0, std::memory_order_relaxed);
-
-    if(node->num_dependents() == 0) {
-      tpg->_sources.push_back(node);
-    }
-
-    node->_set_up_join_counter();
-  }
-
-  tpg->_join_counter = tpg->_sources.size();
-
-  if(worker) {
-    _schedule(*worker, tpg->_sources);
-  }
-  else {
-    _schedule(tpg->_sources);
-  }
-}
-
-// Function: _tear_down_topology
-inline void Executor::_tear_down_topology(Worker& worker, Topology* tpg) {
-
-  auto &f = tpg->_taskflow;
-
-  //assert(&tpg == &(f._topologies.front()));
-
-  // case 1: we still need to run the topology again
-  if(!tpg->_is_cancelled && !tpg->_pred()) {
-    //assert(tpg->_join_counter == 0);
-    std::lock_guard<std::mutex> lock(f._mutex);
-    tpg->_join_counter = tpg->_sources.size();
-    _schedule(worker, tpg->_sources);
-  }
-  // case 2: the final run of this topology
-  else {
-
-    // TODO: if the topology is cancelled, need to release all semaphores
-
-    if(tpg->_call != nullptr) {
-      tpg->_call();
-    }
-
-    // If there is another run (interleave between lock)
-    if(std::unique_lock<std::mutex> lock(f._mutex); f._topologies.size()>1) {
-      //assert(tpg->_join_counter == 0);
-
-      // Set the promise
-      tpg->_promise.set_value();
-      f._topologies.pop();
-      tpg = f._topologies.front().get();
-
-      // decrement the topology but since this is not the last we don't notify
-      _decrement_topology();
-
-      // set up topology needs to be under the lock or it can
-      // introduce memory order error with pop
-      _set_up_topology(&worker, tpg);
-    }
-    else {
-      //assert(f._topologies.size() == 1);
-
-      // Need to back up the promise first here becuz taskflow might be
-      // destroy soon after calling get
-      auto p {std::move(tpg->_promise)};
-
-      // Back up lambda capture in case it has the topology pointer,
-      // to avoid it releasing on pop_front ahead of _mutex.unlock &
-      // _promise.set_value. Released safely when leaving scope.
-      auto c {std::move(tpg->_call)};
-
-      // Get the satellite if any
-      auto s {f._satellite};
-
-      // Now we remove the topology from this taskflow
-      f._topologies.pop();
-
-      //f._mutex.unlock();
-      lock.unlock();
-
-      // We set the promise in the end in case taskflow leaves the scope.
-      // After set_value, the caller will return from wait
-      p.set_value();
-
-      _decrement_topology_and_notify();
-
-      // remove the taskflow if it is managed by the executor
-      // TODO: in the future, we may need to synchronize on wait
-      // (which means the following code should the moved before set_value)
-      if(s) {
-        std::scoped_lock<std::mutex> lock(_taskflow_mutex);
-        _taskflows.erase(*s);
-      }
-    }
-  }
-}
-
-// ############################################################################
-// Forward Declaration: Subflow
-// ############################################################################
-
-inline void Subflow::join() {
-
-  // assert(this_worker().worker == &_worker);
-
-  if(!_joinable) {
-    TF_THROW("subflow not joinable");
-  }
-
-  // only the parent worker can join the subflow
-  _executor._invoke_dynamic_task_external(_worker, _parent, _graph, false);
-  _joinable = false;
-}
-
-inline void Subflow::detach() {
-
-  // assert(this_worker().worker == &_worker);
-
-  if(!_joinable) {
-    TF_THROW("subflow already joined or detached");
-  }
-
-  // only the parent worker can detach the subflow
-  _executor._invoke_dynamic_task_external(_worker, _parent, _graph, true);
-  _joinable = false;
-}
-
-// Function: named_async
-template <typename F, typename... ArgsT>
-auto Subflow::named_async(const std::string& name, F&& f, ArgsT&&... args) {
-  return _named_async(
-    *_executor._this_worker(), name, std::forward<F>(f), std::forward<ArgsT>(args)...
-  );
-}
-
-// Function: _named_async
-template <typename F, typename... ArgsT>
-auto Subflow::_named_async(
-  Worker& w,
-  const std::string& name,
-  F&& f,
-  ArgsT&&... args
-) {
-
-  _parent->_join_counter.fetch_add(1);
-
-  using T = std::invoke_result_t<F, ArgsT...>;
-  using R = std::conditional_t<std::is_same_v<T, void>, void, std::optional<T>>;
-
-  std::promise<R> p;
-
-  auto tpg = std::make_shared<AsyncTopology>();
-
-  Future<R> fu(p.get_future(), tpg);
-
-  auto node = node_pool.animate(
-    std::in_place_type_t<Node::Async>{},
-    [p=make_moc(std::move(p)), f=std::forward<F>(f), args...]
-    (bool cancel) mutable {
-      if constexpr(std::is_same_v<R, void>) {
-        if(!cancel) {
-          f(args...);
-        }
-        p.object.set_value();
-      }
-      else {
-        p.object.set_value(cancel ? std::nullopt : std::make_optional(f(args...)));
-      }
-    },
-    std::move(tpg)
-  );
-
-  node->_name = name;
-  node->_topology = _parent->_topology;
-  node->_parent = _parent;
-
-  _executor._schedule(w, node);
-
-  return fu;
-}
-
-// Function: async
-template <typename F, typename... ArgsT>
-auto Subflow::async(F&& f, ArgsT&&... args) {
-  return named_async("", std::forward<F>(f), std::forward<ArgsT>(args)...);
-}
-
-// Function: _named_silent_async
-template <typename F, typename... ArgsT>
-void Subflow::_named_silent_async(
-  Worker& w, const std::string& name, F&& f, ArgsT&&... args
-) {
-
-  _parent->_join_counter.fetch_add(1);
-
-  auto node = node_pool.animate(
-    std::in_place_type_t<Node::SilentAsync>{},
-    [f=std::forward<F>(f), args...] () mutable {
-      f(args...);
-    }
-  );
-
-  node->_name = name;
-  node->_topology = _parent->_topology;
-  node->_parent = _parent;
-
-  _executor._schedule(w, node);
-}
-
-// Function: silent_async
-template <typename F, typename... ArgsT>
-void Subflow::named_silent_async(const std::string& name, F&& f, ArgsT&&... args) {
-  _named_silent_async(
-    *_executor._this_worker(), name, std::forward<F>(f), std::forward<ArgsT>(args)...
-  );
-}
-
-// Function: named_silent_async
-template <typename F, typename... ArgsT>
-void Subflow::silent_async(F&& f, ArgsT&&... args) {
-  named_silent_async("", std::forward<F>(f), std::forward<ArgsT>(args)...);
-}
-
-// ############################################################################
-// Forward Declaration: Runtime
-// ############################################################################
-
-// Procedure: schedule
-inline void Runtime::schedule(Task task) {
-  auto node = task._node;
-  auto& j = node->_parent ? node->_parent->_join_counter :
-                            node->_topology->_join_counter;
-  j.fetch_add(1);
-  _executor._schedule(_worker, node);
-}
-
-// Procedure: run
-template <typename C>
-void Runtime::run(C&& callable) {
-
-  // dynamic task (subflow)
-  if constexpr(is_dynamic_task_v<C>) {
-    Graph graph;
-    Subflow sf(_executor, _worker, _parent, graph);
-    callable(sf);
-    if(sf._joinable) {
-      _executor._invoke_dynamic_task_internal(_worker, _parent, graph);
-    }
-  }
-  else {
-    static_assert(dependent_false_v<C>, "unsupported task callable to run");
-  }
-}
-
-}  // end of namespace tf -----------------------------------------------------
-
-
-
-
-
-
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/core/graph.hpp b/bundled/taskflow-3.7.0/taskflow/core/graph.hpp
deleted file mode 100644 (file)
index efaa4ff..0000000
+++ /dev/null
@@ -1,1169 +0,0 @@
-#pragma once
-
-#include "../utility/traits.hpp"
-#include "../utility/iterator.hpp"
-#include "../utility/object_pool.hpp"
-#include "../utility/os.hpp"
-#include "../utility/math.hpp"
-#include "../utility/small_vector.hpp"
-#include "../utility/serializer.hpp"
-#include "error.hpp"
-#include "declarations.hpp"
-#include "semaphore.hpp"
-#include "environment.hpp"
-#include "topology.hpp"
-#include "tsq.hpp"
-
-/**
-@file graph.hpp
-@brief graph include file
-*/
-
-namespace tf {
-
-// ----------------------------------------------------------------------------
-// Class: Graph
-// ----------------------------------------------------------------------------
-
-
-/**
-@class Graph
-
-@brief class to create a graph object
-
-A graph is the ultimate storage for a task dependency graph and is the main
-gateway to interact with an executor.
-A graph manages a set of nodes in a global object pool that animates and
-recycles node objects efficiently without going through repetitive and
-expensive memory allocations and deallocations.
-This class is mainly used for creating an opaque graph object in a custom
-class to interact with the executor through taskflow composition.
-
-A graph object is move-only.
-*/
-class Graph {
-
-  friend class Node;
-  friend class FlowBuilder;
-  friend class Subflow;
-  friend class Taskflow;
-  friend class Executor;
-
-  public:
-
-    /**
-    @brief constructs a graph object
-    */
-    Graph() = default;
-
-    /**
-    @brief disabled copy constructor
-    */
-    Graph(const Graph&) = delete;
-
-    /**
-    @brief constructs a graph using move semantics
-    */
-    Graph(Graph&&);
-
-    /**
-    @brief destructs the graph object
-    */
-    ~Graph();
-
-    /**
-    @brief disabled copy assignment operator
-    */
-    Graph& operator = (const Graph&) = delete;
-
-    /**
-    @brief assigns a graph using move semantics
-    */
-    Graph& operator = (Graph&&);
-
-    /**
-    @brief queries if the graph is empty
-    */
-    bool empty() const;
-
-    /**
-    @brief queries the number of nodes in the graph
-    */
-    size_t size() const;
-
-    /**
-    @brief clears the graph
-    */
-    void clear();
-
-  private:
-
-    std::vector<Node*> _nodes;
-
-    void _clear();
-    void _clear_detached();
-    void _merge(Graph&&);
-    void _erase(Node*);
-    
-    /**
-    @private
-    */
-    template <typename ...ArgsT>
-    Node* _emplace_back(ArgsT&&...);
-};
-
-// ----------------------------------------------------------------------------
-
-/**
-@class Runtime
-
-@brief class to include a runtime object in a task
-
-A runtime object allows users to interact with the
-scheduling runtime inside a task, such as scheduling an active task,
-spawning a subflow, and so on.
-
-@code{.cpp}
-tf::Task A, B, C, D;
-std::tie(A, B, C, D) = taskflow.emplace(
-  [] () { return 0; },
-  [&C] (tf::Runtime& rt) {  // C must be captured by reference
-    std::cout << "B\n";
-    rt.schedule(C);
-  },
-  [] () { std::cout << "C\n"; },
-  [] () { std::cout << "D\n"; }
-);
-A.precede(B, C, D);
-executor.run(taskflow).wait();
-@endcode
-
-A runtime object is associated with the worker and the executor
-that runs the task.
-
-*/
-class Runtime {
-
-  friend class Executor;
-  friend class FlowBuilder;
-
-  public:
-  
-  /**
-  @brief destroys the runtime object
-
-  Issues a tf::Runtime::corun_all to finish all spawned asynchronous tasks
-  and then destroys the runtime object.
-  */
-  ~Runtime();
-
-  /**
-  @brief obtains the running executor
-
-  The running executor of a runtime task is the executor that runs
-  the parent taskflow of that runtime task.
-
-  @code{.cpp}
-  tf::Executor executor;
-  tf::Taskflow taskflow;
-  taskflow.emplace([&](tf::Runtime& rt){
-    assert(&(rt.executor()) == &executor);
-  });
-  executor.run(taskflow).wait();
-  @endcode
-  */
-  Executor& executor();
-
-  /**
-  @brief schedules an active task immediately to the worker's queue
-
-  @param task the given active task to schedule immediately
-
-  This member function immediately schedules an active task to the
-  task queue of the associated worker in the runtime task.
-  An active task is a task in a running taskflow.
-  The task may or may not be running, and scheduling that task
-  will immediately put the task into the task queue of the worker
-  that is running the runtime task.
-  Consider the following example:
-
-  @code{.cpp}
-  tf::Task A, B, C, D;
-  std::tie(A, B, C, D) = taskflow.emplace(
-    [] () { return 0; },
-    [&C] (tf::Runtime& rt) {  // C must be captured by reference
-      std::cout << "B\n";
-      rt.schedule(C);
-    },
-    [] () { std::cout << "C\n"; },
-    [] () { std::cout << "D\n"; }
-  );
-  A.precede(B, C, D);
-  executor.run(taskflow).wait();
-  @endcode
-
-  The executor will first run the condition task @c A which returns @c 0
-  to inform the scheduler to go to the runtime task @c B.
-  During the execution of @c B, it directly schedules task @c C without
-  going through the normal taskflow graph scheduling process.
-  At this moment, task @c C is active because its parent taskflow is running.
-  When the taskflow finishes, we will see both @c B and @c C in the output.
-  */
-  void schedule(Task task);
-  
-  /**
-  @brief runs the given callable asynchronously
-
-  @tparam F callable type
-  @param f callable object
-    
-  The method creates an asynchronous task to launch the given
-  function on the given arguments.
-  The difference to tf::Executor::async is that the created asynchronous task
-  pertains to the runtime object.
-  Applications can explicitly issue tf::Runtime::corun_all
-  to wait for all spawned asynchronous tasks to finish.
-  For example:
-
-  @code{.cpp}
-  std::atomic<int> counter(0);
-  taskflow.emplace([&](tf::Runtime& rt){
-    auto fu1 = rt.async([&](){ counter++; });
-    auto fu2 = rt.async([&](){ counter++; });
-    fu1.get();
-    fu2.get();
-    assert(counter == 2);
-    
-    // spawn 100 asynchronous tasks from the worker of the runtime
-    for(int i=0; i<100; i++) {
-      rt.async([&](){ counter++; });
-    }
-    
-    // wait for the 100 asynchronous tasks to finish
-    rt.corun_all();
-    assert(counter == 102);
-  });
-  @endcode
-
-  This method is thread-safe and can be called by multiple workers
-  that hold the reference to the runtime.
-  For example, the code below spawns 100 tasks from the worker of
-  a runtime, and each of the 100 tasks spawns another task
-  that will be run by another worker.
-  
-  @code{.cpp}
-  std::atomic<int> counter(0);
-  taskflow.emplace([&](tf::Runtime& rt){
-    // worker of the runtime spawns 100 tasks each spawning another task
-    // that will be run by another worker
-    for(int i=0; i<100; i++) {
-      rt.async([&](){ 
-        counter++; 
-        rt.async([](){ counter++; });
-      });
-    }
-    
-    // wait for the 200 asynchronous tasks to finish
-    rt.corun_all();
-    assert(counter == 200);
-  });
-  @endcode
-  */
-  template <typename F>
-  auto async(F&& f);
-  
-  /**
-  @brief runs the given callable asynchronously
-
-  @tparam F callable type
-  @tparam P task parameters type
-
-  @param params task parameters
-  @param f callable
-
-  @code{.cpp}
-  taskflow.emplace([&](tf::Runtime& rt){
-    auto future = rt.async("my task", [](){});
-    future.get();
-  });
-  @endcode
-
-  */
-  template <typename P, typename F>
-  auto async(P&& params, F&& f);
-
-  /**
-  @brief runs the given function asynchronously without returning any future object
-
-  @tparam F callable type
-  @param f callable
-
-  This member function is more efficient than tf::Runtime::async
-  and is encouraged to use when there is no data returned.
-
-  @code{.cpp}
-  std::atomic<int> counter(0);
-  taskflow.emplace([&](tf::Runtime& rt){
-    for(int i=0; i<100; i++) {
-      rt.silent_async([&](){ counter++; });
-    }
-    rt.corun_all();
-    assert(counter == 100);
-  });
-  @endcode
-
-  This member function is thread-safe.
-  */
-  template <typename F>
-  void silent_async(F&& f);
-  
-  /**
-  @brief runs the given function asynchronously without returning any future object
-
-  @tparam F callable type
-  @param params task parameters
-  @param f callable
-  
-  @code{.cpp}
-  taskflow.emplace([&](tf::Runtime& rt){
-    rt.silent_async("my task", [](){});
-    rt.corun_all();
-  });
-  @endcode
-  */
-  template <typename P, typename F>
-  void silent_async(P&& params, F&& f);
-  
-  /**
-  @brief similar to tf::Runtime::silent_async but the caller must be the worker of the runtime
-
-  @tparam F callable type
-
-  @param f callable
-
-  The method bypass the check of the caller worker from the executor 
-  and thus can only called by the worker of this runtime.
-
-  @code{.cpp}
-  taskflow.emplace([&](tf::Runtime& rt){
-    // running by the worker of this runtime
-    rt.silent_async_unchecked([](){});
-    rt.corun_all();
-  });
-  @endcode
-  */
-  template <typename F>
-  void silent_async_unchecked(F&& f);
-  
-  /**
-  @brief similar to tf::Runtime::silent_async but the caller must be the worker of the runtime
-
-  @tparam F callable type
-  @tparam P task parameters type
-
-  @param params task parameters
-  @param f callable
-
-  The method bypass the check of the caller worker from the executor 
-  and thus can only called by the worker of this runtime.
-
-  @code{.cpp}
-  taskflow.emplace([&](tf::Runtime& rt){
-    // running by the worker of this runtime
-    rt.silent_async_unchecked("my task", [](){});
-    rt.corun_all();
-  });
-  @endcode
-  */
-  template <typename P, typename F>
-  void silent_async_unchecked(P&& params, F&& f);
-
-  /**
-  @brief co-runs the given target and waits until it completes
-  
-  A target can be one of the following forms:
-    + a subflow task to spawn a subflow or
-    + a composable graph object with `tf::Graph& T::graph()` defined
-
-  @code{.cpp}
-  // co-run a subflow and wait until all tasks complete
-  taskflow.emplace([](tf::Runtime& rt){
-    rt.corun([](tf::Subflow& sf){
-      tf::Task A = sf.emplace([](){});
-      tf::Task B = sf.emplace([](){});
-    }); 
-  });
-  
-  // co-run a taskflow and wait until all tasks complete
-  tf::Taskflow taskflow1, taskflow2;
-  taskflow1.emplace([](){ std::cout << "running taskflow1\n"; });
-  taskflow2.emplace([&](tf::Runtime& rt){
-    std::cout << "running taskflow2\n";
-    rt.corun(taskflow1);
-  });
-  executor.run(taskflow2).wait();
-  @endcode
-
-  Although tf::Runtime::corun blocks until the operation completes, 
-  the caller thread (worker) is not blocked (e.g., sleeping or holding any lock). 
-  Instead, the caller thread joins the work-stealing loop of the executor 
-  and returns when all tasks in the target completes.
-  
-  @attention
-  Only the worker of this tf::Runtime can issue corun.
-  */
-  template <typename T>
-  void corun(T&& target);
-
-  /**
-  @brief keeps running the work-stealing loop until the predicate becomes true
-  
-  @tparam P predicate type
-  @param predicate a boolean predicate to indicate when to stop the loop
-
-  The method keeps the caller worker running in the work-stealing loop
-  until the stop predicate becomes true.
-  
-  @attention
-  Only the worker of this tf::Runtime can issue corun.
-  */
-  template <typename P>
-  void corun_until(P&& predicate);
-  
-  /**
-  @brief corun all asynchronous tasks spawned by this runtime with other workers
-
-  Coruns all asynchronous tasks (tf::Runtime::async,
-  tf::Runtime::silent_async) with other workers until all those 
-  asynchronous tasks finish.
-    
-  @code{.cpp}
-  std::atomic<size_t> counter{0};
-  taskflow.emplace([&](tf::Runtime& rt){
-    // spawn 100 async tasks and wait
-    for(int i=0; i<100; i++) {
-      rt.silent_async([&](){ counter++; });
-    }
-    rt.corun_all();
-    assert(counter == 100);
-    
-    // spawn another 100 async tasks and wait
-    for(int i=0; i<100; i++) {
-      rt.silent_async([&](){ counter++; });
-    }
-    rt.corun_all();
-    assert(counter == 200);
-  });
-  @endcode
-
-  @attention
-  Only the worker of this tf::Runtime can issue tf::Runtime::corun_all.
-  */
-  inline void corun_all();
-
-  /**
-  @brief acquire a reference to the underlying worker
-  */
-  inline Worker& worker();
-
-  protected:
-  
-  /**
-  @private
-  */
-  explicit Runtime(Executor&, Worker&, Node*);
-  
-  /**
-  @private
-  */
-  Executor& _executor;
-  
-  /**
-  @private
-  */
-  Worker& _worker;
-  
-  /**
-  @private
-  */
-  Node* _parent;
-
-  /**
-  @private
-  */
-  template <typename P, typename F>
-  auto _async(Worker& w, P&& params, F&& f);
-  
-  /**
-  @private
-  */
-  template <typename P, typename F>
-  void _silent_async(Worker& w, P&& params, F&& f);
-};
-
-// constructor
-inline Runtime::Runtime(Executor& e, Worker& w, Node* p) :
-  _executor{e},
-  _worker  {w},
-  _parent  {p}{
-}
-
-// Function: executor
-inline Executor& Runtime::executor() {
-  return _executor;
-}
-
-// Function: worker
-inline Worker& Runtime::worker() {
-  return _worker;
-}
-
-// ----------------------------------------------------------------------------
-// TaskParams
-// ----------------------------------------------------------------------------
-
-/**
-@struct TaskParams
-
-@brief task parameters to use when creating an asynchronous task
-*/
-struct TaskParams {
-  /**
-  @brief name of the task
-  */
-  std::string name;
-
-  /**
-  @brief priority of the tassk
-  */
-  unsigned priority {0};
-
-  /**
-  @brief C-styled pointer to user data
-  */
-  void* data {nullptr};
-};
-
-/**
-@struct DefaultTaskParams
-
-@brief empty task parameter type for compile-time optimization
-*/
-struct DefaultTaskParams {
-};
-
-/**
-@brief determines if the given type is a task parameter type
-
-Task parameters can be specified in one of the following types:
-  + tf::TaskParams: assign the struct of defined parameters
-  + tf::DefaultTaskParams: assign nothing
-  + std::string: assign a name to the task
-*/
-template <typename P>
-constexpr bool is_task_params_v =
-  std::is_same_v<std::decay_t<P>, TaskParams> ||
-  std::is_same_v<std::decay_t<P>, DefaultTaskParams> ||
-  std::is_constructible_v<std::string, P>;
-
-// ----------------------------------------------------------------------------
-// Node
-// ----------------------------------------------------------------------------
-
-/**
-@private
-*/
-class Node {
-
-  friend class Graph;
-  friend class Task;
-  friend class AsyncTask;
-  friend class TaskView;
-  friend class Taskflow;
-  friend class Executor;
-  friend class FlowBuilder;
-  friend class Subflow;
-  friend class Runtime;
-
-  enum class AsyncState : int {
-    UNFINISHED = 0,
-    LOCKED = 1,
-    FINISHED = 2
-  };
-
-  TF_ENABLE_POOLABLE_ON_THIS;
-
-  // state bit flag
-  constexpr static int CONDITIONED = 1;
-  constexpr static int DETACHED    = 2;
-  constexpr static int ACQUIRED    = 4;
-  constexpr static int READY       = 8;
-  constexpr static int EXCEPTION   = 16;
-
-  using Placeholder = std::monostate;
-
-  // static work handle
-  struct Static {
-
-    template <typename C>
-    Static(C&&);
-
-    std::variant<
-      std::function<void()>, std::function<void(Runtime&)>
-    > work;
-  };
-
-  // subflow work handle
-  struct Subflow {
-
-    template <typename C>
-    Subflow(C&&);
-
-    std::function<void(tf::Subflow&)> work;
-    Graph subgraph;
-  };
-
-  // condition work handle
-  struct Condition {
-
-    template <typename C>
-    Condition(C&&);
-    
-    std::variant<
-      std::function<int()>, std::function<int(Runtime&)>
-    > work;
-  };
-
-  // multi-condition work handle
-  struct MultiCondition {
-
-    template <typename C>
-    MultiCondition(C&&);
-
-    std::variant<
-      std::function<SmallVector<int>()>, std::function<SmallVector<int>(Runtime&)>
-    > work;
-  };
-
-  // module work handle
-  struct Module {
-
-    template <typename T>
-    Module(T&);
-
-    Graph& graph;
-  };
-
-  // Async work
-  struct Async {
-
-    template <typename T>
-    Async(T&&);
-
-    std::variant<
-      std::function<void()>, std::function<void(Runtime&)>
-    > work;
-  };
-  
-  // silent dependent async
-  struct DependentAsync {
-    
-    template <typename C>
-    DependentAsync(C&&);
-    
-    std::variant<
-      std::function<void()>, std::function<void(Runtime&)>
-    > work;
-   
-    std::atomic<size_t> use_count {1};
-    std::atomic<AsyncState> state {AsyncState::UNFINISHED};
-  };
-
-  using handle_t = std::variant<
-    Placeholder,      // placeholder
-    Static,           // static tasking
-    Subflow,          // subflow tasking
-    Condition,        // conditional tasking
-    MultiCondition,   // multi-conditional tasking
-    Module,           // composable tasking
-    Async,            // async tasking
-    DependentAsync    // dependent async tasking
-  >;
-
-  struct Semaphores {
-    SmallVector<Semaphore*> to_acquire;
-    SmallVector<Semaphore*> to_release;
-  };
-
-  public:
-
-  // variant index
-  constexpr static auto PLACEHOLDER     = get_index_v<Placeholder, handle_t>;
-  constexpr static auto STATIC          = get_index_v<Static, handle_t>;
-  constexpr static auto SUBFLOW         = get_index_v<Subflow, handle_t>;
-  constexpr static auto CONDITION       = get_index_v<Condition, handle_t>;
-  constexpr static auto MULTI_CONDITION = get_index_v<MultiCondition, handle_t>;
-  constexpr static auto MODULE          = get_index_v<Module, handle_t>;
-  constexpr static auto ASYNC           = get_index_v<Async, handle_t>;
-  constexpr static auto DEPENDENT_ASYNC = get_index_v<DependentAsync, handle_t>;
-
-  Node() = default;
-
-  template <typename... Args>
-  Node(const std::string&, unsigned, Topology*, Node*, size_t, Args&&...);
-  
-  template <typename... Args>
-  Node(const std::string&, Topology*, Node*, size_t, Args&&...);
-  
-  template <typename... Args>
-  Node(const TaskParams&, Topology*, Node*, size_t, Args&&...);
-  
-  template <typename... Args>
-  Node(const DefaultTaskParams&, Topology*, Node*, size_t, Args&&...);
-
-  ~Node();
-
-  size_t num_successors() const;
-  size_t num_dependents() const;
-  size_t num_strong_dependents() const;
-  size_t num_weak_dependents() const;
-
-  const std::string& name() const;
-
-  private:
-
-  std::string _name;
-  
-  unsigned _priority {0};
-  
-  void* _data {nullptr};
-  
-  Topology* _topology {nullptr};
-  Node* _parent {nullptr};
-
-  SmallVector<Node*> _successors;
-  SmallVector<Node*> _dependents;
-
-  std::atomic<int> _state {0};
-  std::atomic<size_t> _join_counter {0};
-
-  std::unique_ptr<Semaphores> _semaphores;
-  std::exception_ptr _exception_ptr {nullptr};
-  
-  handle_t _handle;
-
-  void _precede(Node*);
-  void _set_up_join_counter();
-  void _process_exception();
-
-  bool _is_cancelled() const;
-  bool _is_conditioner() const;
-  bool _acquire_all(SmallVector<Node*>&);
-
-  SmallVector<Node*> _release_all();
-};
-
-// ----------------------------------------------------------------------------
-// Node Object Pool
-// ----------------------------------------------------------------------------
-
-/**
-@private
-*/
-inline ObjectPool<Node> node_pool;
-
-// ----------------------------------------------------------------------------
-// Definition for Node::Static
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename C>
-Node::Static::Static(C&& c) : work {std::forward<C>(c)} {
-}
-
-// ----------------------------------------------------------------------------
-// Definition for Node::Subflow
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename C>
-Node::Subflow::Subflow(C&& c) : work {std::forward<C>(c)} {
-}
-
-// ----------------------------------------------------------------------------
-// Definition for Node::Condition
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename C>
-Node::Condition::Condition(C&& c) : work {std::forward<C>(c)} {
-}                                        
-
-// ----------------------------------------------------------------------------
-// Definition for Node::MultiCondition
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename C>
-Node::MultiCondition::MultiCondition(C&& c) : work {std::forward<C>(c)} {
-}
-
-// ----------------------------------------------------------------------------
-// Definition for Node::Module
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename T>
-inline Node::Module::Module(T& obj) : graph{ obj.graph() } {
-}
-
-// ----------------------------------------------------------------------------
-// Definition for Node::Async
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename C>
-Node::Async::Async(C&& c) : work {std::forward<C>(c)} {
-}
-
-// ----------------------------------------------------------------------------
-// Definition for Node::DependentAsync
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename C>
-Node::DependentAsync::DependentAsync(C&& c) : work {std::forward<C>(c)} {
-}
-
-// ----------------------------------------------------------------------------
-// Definition for Node
-// ----------------------------------------------------------------------------
-
-// Constructor
-template <typename... Args>
-Node::Node(
-  const std::string& name, 
-  unsigned priority,
-  Topology* topology, 
-  Node* parent, 
-  size_t join_counter,
-  Args&&... args
-) :
-  _name         {name},
-  _priority     {priority},
-  _topology     {topology},
-  _parent       {parent},
-  _join_counter {join_counter},
-  _handle       {std::forward<Args>(args)...} {
-}
-
-// Constructor
-template <typename... Args>
-Node::Node(
-  const std::string& name,
-  Topology* topology, 
-  Node* parent, 
-  size_t join_counter,
-  Args&&... args
-) :
-  _name         {name},
-  _topology     {topology},
-  _parent       {parent},
-  _join_counter {join_counter},
-  _handle       {std::forward<Args>(args)...} {
-}
-
-// Constructor
-template <typename... Args>
-Node::Node(
-  const TaskParams& params,
-  Topology* topology, 
-  Node* parent, 
-  size_t join_counter,
-  Args&&... args
-) :
-  _name         {params.name},
-  _priority     {params.priority},
-  _data         {params.data},
-  _topology     {topology},
-  _parent       {parent},
-  _join_counter {join_counter},
-  _handle       {std::forward<Args>(args)...} {
-}
-
-// Constructor
-template <typename... Args>
-Node::Node(
-  const DefaultTaskParams&,
-  Topology* topology, 
-  Node* parent, 
-  size_t join_counter,
-  Args&&... args
-) :
-  _topology     {topology},
-  _parent       {parent},
-  _join_counter {join_counter},
-  _handle       {std::forward<Args>(args)...} {
-}
-
-// Destructor
-inline Node::~Node() {
-  // this is to avoid stack overflow
-
-  if(_handle.index() == SUBFLOW) {
-    // using std::get_if instead of std::get makes this compatible
-    // with older macOS versions
-    // the result of std::get_if is guaranteed to be non-null
-    // due to the index check above
-    auto& subgraph = std::get_if<Subflow>(&_handle)->subgraph;
-    std::vector<Node*> nodes;
-    nodes.reserve(subgraph.size());
-
-    std::move(
-      subgraph._nodes.begin(), subgraph._nodes.end(), std::back_inserter(nodes)
-    );
-    subgraph._nodes.clear();
-
-    size_t i = 0;
-
-    while(i < nodes.size()) {
-
-      if(nodes[i]->_handle.index() == SUBFLOW) {
-        auto& sbg = std::get_if<Subflow>(&(nodes[i]->_handle))->subgraph;
-        std::move(
-          sbg._nodes.begin(), sbg._nodes.end(), std::back_inserter(nodes)
-        );
-        sbg._nodes.clear();
-      }
-
-      ++i;
-    }
-
-    //auto& np = Graph::_node_pool();
-    for(i=0; i<nodes.size(); ++i) {
-      node_pool.recycle(nodes[i]);
-    }
-  }
-}
-
-// Procedure: _precede
-inline void Node::_precede(Node* v) {
-  _successors.push_back(v);
-  v->_dependents.push_back(this);
-}
-
-// Function: num_successors
-inline size_t Node::num_successors() const {
-  return _successors.size();
-}
-
-// Function: dependents
-inline size_t Node::num_dependents() const {
-  return _dependents.size();
-}
-
-// Function: num_weak_dependents
-inline size_t Node::num_weak_dependents() const {
-  size_t n = 0;
-  for(size_t i=0; i<_dependents.size(); i++) {
-    //if(_dependents[i]->_handle.index() == Node::CONDITION) {
-    if(_dependents[i]->_is_conditioner()) {
-      n++;
-    }
-  }
-  return n;
-}
-
-// Function: num_strong_dependents
-inline size_t Node::num_strong_dependents() const {
-  size_t n = 0;
-  for(size_t i=0; i<_dependents.size(); i++) {
-    //if(_dependents[i]->_handle.index() != Node::CONDITION) {
-    if(!_dependents[i]->_is_conditioner()) {
-      n++;
-    }
-  }
-  return n;
-}
-
-// Function: name
-inline const std::string& Node::name() const {
-  return _name;
-}
-
-// Function: _is_conditioner
-inline bool Node::_is_conditioner() const {
-  return _handle.index() == Node::CONDITION ||
-         _handle.index() == Node::MULTI_CONDITION;
-}
-
-// Function: _is_cancelled
-// we currently only support cancellation of taskflow (no async task)
-inline bool Node::_is_cancelled() const {
-  //return _topology && _topology->_is_cancelled.load(std::memory_order_relaxed);
-  return _topology &&
-         (_topology->_state.load(std::memory_order_relaxed) & Topology::CANCELLED);
-}
-
-// Procedure: _set_up_join_counter
-inline void Node::_set_up_join_counter() {
-  size_t c = 0;
-  for(auto p : _dependents) {
-    //if(p->_handle.index() == Node::CONDITION) {
-    if(p->_is_conditioner()) {
-      _state.fetch_or(Node::CONDITIONED, std::memory_order_relaxed);
-    }
-    else {
-      c++;
-    }
-  }
-  _join_counter.store(c, std::memory_order_relaxed);
-}
-
-// Procedure: _process_exception
-inline void Node::_process_exception() {
-  if(_exception_ptr) {
-    auto e = _exception_ptr;
-    _exception_ptr = nullptr;
-    std::rethrow_exception(e);
-  }
-}
-
-// Function: _acquire_all
-inline bool Node::_acquire_all(SmallVector<Node*>& nodes) {
-
-  auto& to_acquire = _semaphores->to_acquire;
-
-  for(size_t i = 0; i < to_acquire.size(); ++i) {
-    if(!to_acquire[i]->_try_acquire_or_wait(this)) {
-      for(size_t j = 1; j <= i; ++j) {
-        auto r = to_acquire[i-j]->_release();
-        nodes.insert(std::end(nodes), std::begin(r), std::end(r));
-      }
-      return false;
-    }
-  }
-  return true;
-}
-
-// Function: _release_all
-inline SmallVector<Node*> Node::_release_all() {
-
-  auto& to_release = _semaphores->to_release;
-
-  SmallVector<Node*> nodes;
-  for(const auto& sem : to_release) {
-    auto r = sem->_release();
-    nodes.insert(std::end(nodes), std::begin(r), std::end(r));
-  }
-
-  return nodes;
-}
-
-// ----------------------------------------------------------------------------
-// Node Deleter
-// ----------------------------------------------------------------------------
-
-/**
-@private
-*/
-struct NodeDeleter {
-  void operator ()(Node* ptr) {
-    node_pool.recycle(ptr);
-  }
-};
-
-// ----------------------------------------------------------------------------
-// Graph definition
-// ----------------------------------------------------------------------------
-
-// Destructor
-inline Graph::~Graph() {
-  _clear();
-}
-
-// Move constructor
-inline Graph::Graph(Graph&& other) :
-  _nodes {std::move(other._nodes)} {
-}
-
-// Move assignment
-inline Graph& Graph::operator = (Graph&& other) {
-  _clear();
-  _nodes = std::move(other._nodes);
-  return *this;
-}
-
-// Procedure: clear
-inline void Graph::clear() {
-  _clear();
-}
-
-// Procedure: clear
-inline void Graph::_clear() {
-  for(auto node : _nodes) {
-    node_pool.recycle(node);
-  }
-  _nodes.clear();
-}
-
-// Procedure: clear_detached
-inline void Graph::_clear_detached() {
-
-  auto mid = std::partition(_nodes.begin(), _nodes.end(), [] (Node* node) {
-    return !(node->_state.load(std::memory_order_relaxed) & Node::DETACHED);
-  });
-
-  for(auto itr = mid; itr != _nodes.end(); ++itr) {
-    node_pool.recycle(*itr);
-  }
-  _nodes.resize(std::distance(_nodes.begin(), mid));
-}
-
-// Procedure: merge
-inline void Graph::_merge(Graph&& g) {
-  for(auto n : g._nodes) {
-    _nodes.push_back(n);
-  }
-  g._nodes.clear();
-}
-
-// Function: erase
-inline void Graph::_erase(Node* node) {
-  if(auto I = std::find(_nodes.begin(), _nodes.end(), node); I != _nodes.end()) {
-    _nodes.erase(I);
-    node_pool.recycle(node);
-  }
-}
-
-// Function: size
-inline size_t Graph::size() const {
-  return _nodes.size();
-}
-
-// Function: empty
-inline bool Graph::empty() const {
-  return _nodes.empty();
-}
-
-/**
-@private
-*/
-template <typename ...ArgsT>
-Node* Graph::_emplace_back(ArgsT&&... args) {
-  _nodes.push_back(node_pool.animate(std::forward<ArgsT>(args)...));
-  return _nodes.back();
-}
-
-
-}  // end of namespace tf. ---------------------------------------------------
-
-
-
-
-
-
-
-
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/core/tsq.hpp b/bundled/taskflow-3.7.0/taskflow/core/tsq.hpp
deleted file mode 100644 (file)
index e4ea76c..0000000
+++ /dev/null
@@ -1,441 +0,0 @@
-#pragma once
-
-#include "../utility/macros.hpp"
-#include "../utility/traits.hpp"
-
-/**
-@file tsq.hpp
-@brief task queue include file
-*/
-
-namespace tf {
-
-
-// ----------------------------------------------------------------------------
-// Task Types
-// ----------------------------------------------------------------------------
-
-/**
-@enum TaskPriority
-
-@brief enumeration of all task priority values
-
-A priority is an enumerated value of type @c unsigned.
-Currently, %Taskflow defines three priority levels, 
-@c HIGH, @c NORMAL, and @c LOW, starting from 0, 1, to 2.
-That is, the lower the value, the higher the priority.
-
-*/
-enum class TaskPriority : unsigned {
-  /** @brief value of the highest priority (i.e., 0)  */
-  HIGH = 0,
-  /** @brief value of the normal priority (i.e., 1)  */
-  NORMAL = 1,
-  /** @brief value of the lowest priority (i.e., 2) */
-  LOW = 2,
-  /** @brief conventional value for iterating priority values */
-  MAX = 3
-};
-
-
-
-// ----------------------------------------------------------------------------
-// Task Queue
-// ----------------------------------------------------------------------------
-
-
-/**
-@class: TaskQueue
-
-@tparam T data type (must be a pointer type)
-@tparam TF_MAX_PRIORITY maximum level of the priority 
-
-@brief class to create a lock-free unbounded single-producer multiple-consumer queue
-
-This class implements the work-stealing queue described in the paper,
-<a href="https://www.di.ens.fr/~zappa/readings/ppopp13.pdf">Correct and Efficient Work-Stealing for Weak Memory Models</a>,
-and extends it to include priority.
-
-Only the queue owner can perform pop and push operations,
-while others can steal data from the queue simultaneously.
-Priority starts from zero (highest priority) to the template value 
-`TF_MAX_PRIORITY-1` (lowest priority).
-All operations are associated with priority values to indicate
-the corresponding queues to which an operation is applied.
-
-The default template value, `TF_MAX_PRIORITY`, is `TaskPriority::MAX` 
-which applies only three priority levels to the task queue.
-
-@code{.cpp}
-auto [A, B, C, D, E] = taskflow.emplace(
-  [] () { },
-  [&] () { 
-    std::cout << "Task B: " << counter++ << '\n';  // 0
-  },
-  [&] () { 
-    std::cout << "Task C: " << counter++ << '\n';  // 2
-  },
-  [&] () { 
-    std::cout << "Task D: " << counter++ << '\n';  // 1
-  },
-  [] () { }
-);
-
-A.precede(B, C, D); 
-E.succeed(B, C, D);
-  
-B.priority(tf::TaskPriority::HIGH);
-C.priority(tf::TaskPriority::LOW);
-D.priority(tf::TaskPriority::NORMAL);
-  
-executor.run(taskflow).wait();
-@endcode
-
-In the above example, we have a task graph of five tasks,
-@c A, @c B, @c C, @c D, and @c E, in which @c B, @c C, and @c D
-can run in simultaneously when @c A finishes.
-Since we only uses one worker thread in the executor, 
-we can deterministically run @c B first, then @c D, and @c C
-in order of their priority values.
-The output is as follows:
-
-@code{.shell-session}
-Task B: 0
-Task D: 1
-Task C: 2
-@endcode
-
-*/
-template <typename T, unsigned TF_MAX_PRIORITY = static_cast<unsigned>(TaskPriority::MAX)>
-class TaskQueue {
-  
-  static_assert(TF_MAX_PRIORITY > 0, "TF_MAX_PRIORITY must be at least one");
-  static_assert(std::is_pointer_v<T>, "T must be a pointer type");
-
-  struct Array {
-
-    int64_t C;
-    int64_t M;
-    std::atomic<T>* S;
-
-    explicit Array(int64_t c) :
-      C {c},
-      M {c-1},
-      S {new std::atomic<T>[static_cast<size_t>(C)]} {
-    }
-
-    ~Array() {
-      delete [] S;
-    }
-
-    int64_t capacity() const noexcept {
-      return C;
-    }
-
-    void push(int64_t i, T o) noexcept {
-      S[i & M].store(o, std::memory_order_relaxed);
-    }
-
-    T pop(int64_t i) noexcept {
-      return S[i & M].load(std::memory_order_relaxed);
-    }
-
-    Array* resize(int64_t b, int64_t t) {
-      Array* ptr = new Array {2*C};
-      for(int64_t i=t; i!=b; ++i) {
-        ptr->push(i, pop(i));
-      }
-      return ptr;
-    }
-
-  };
-
-  // Doubling the alignment by 2 seems to generate the most
-  // decent performance.
-  CachelineAligned<std::atomic<int64_t>> _top[TF_MAX_PRIORITY];
-  CachelineAligned<std::atomic<int64_t>> _bottom[TF_MAX_PRIORITY];
-  std::atomic<Array*> _array[TF_MAX_PRIORITY];
-  std::vector<Array*> _garbage[TF_MAX_PRIORITY];
-
-  //std::atomic<T> _cache {nullptr};
-
-  public:
-
-    /**
-    @brief constructs the queue with a given capacity
-
-    @param capacity the capacity of the queue (must be power of 2)
-    */
-    explicit TaskQueue(int64_t capacity = 512);
-
-    /**
-    @brief destructs the queue
-    */
-    ~TaskQueue();
-
-    /**
-    @brief queries if the queue is empty at the time of this call
-    */
-    bool empty() const noexcept;
-
-    /**
-    @brief queries if the queue is empty at a specific priority value
-    */
-    bool empty(unsigned priority) const noexcept;
-
-    /**
-    @brief queries the number of items at the time of this call
-    */
-    size_t size() const noexcept;
-
-    /**
-    @brief queries the number of items with the given priority
-           at the time of this call
-    */
-    size_t size(unsigned priority) const noexcept;
-
-    /**
-    @brief queries the capacity of the queue
-    */
-    int64_t capacity() const noexcept;
-    
-    /**
-    @brief queries the capacity of the queue at a specific priority value
-    */
-    int64_t capacity(unsigned priority) const noexcept;
-
-    /**
-    @brief inserts an item to the queue
-
-    @param item the item to push to the queue
-    @param priority priority value of the item to push (default = 0)
-    
-    Only the owner thread can insert an item to the queue.
-    The operation can trigger the queue to resize its capacity
-    if more space is required.
-    */
-    TF_FORCE_INLINE void push(T item, unsigned priority);
-
-    /**
-    @brief pops out an item from the queue
-
-    Only the owner thread can pop out an item from the queue.
-    The return can be a @c nullptr if this operation failed (empty queue).
-    */
-    T pop();
-
-    /**
-    @brief pops out an item with a specific priority value from the queue
-
-    @param priority priority of the item to pop
-
-    Only the owner thread can pop out an item from the queue.
-    The return can be a @c nullptr if this operation failed (empty queue).
-    */
-    TF_FORCE_INLINE T pop(unsigned priority);
-
-    /**
-    @brief steals an item from the queue
-
-    Any threads can try to steal an item from the queue.
-    The return can be a @c nullptr if this operation failed (not necessary empty).
-    */
-    T steal();
-
-    /**
-    @brief steals an item with a specific priority value from the queue
-
-    @param priority priority of the item to steal
-
-    Any threads can try to steal an item from the queue.
-    The return can be a @c nullptr if this operation failed (not necessary empty).
-    */
-    T steal(unsigned priority);
-
-  private:
-    TF_NO_INLINE Array* resize_array(Array* a, unsigned p, std::int64_t b, std::int64_t t);
-};
-
-// Constructor
-template <typename T, unsigned TF_MAX_PRIORITY>
-TaskQueue<T, TF_MAX_PRIORITY>::TaskQueue(int64_t c) {
-  assert(c && (!(c & (c-1))));
-  unroll<0, TF_MAX_PRIORITY, 1>([&](auto p){
-    _top[p].data.store(0, std::memory_order_relaxed);
-    _bottom[p].data.store(0, std::memory_order_relaxed);
-    _array[p].store(new Array{c}, std::memory_order_relaxed);
-    _garbage[p].reserve(32);
-  });
-}
-
-// Destructor
-template <typename T, unsigned TF_MAX_PRIORITY>
-TaskQueue<T, TF_MAX_PRIORITY>::~TaskQueue() {
-  unroll<0, TF_MAX_PRIORITY, 1>([&](auto p){
-    for(auto a : _garbage[p]) {
-      delete a;
-    }
-    delete _array[p].load();
-  });
-}
-
-// Function: empty
-template <typename T, unsigned TF_MAX_PRIORITY>
-bool TaskQueue<T, TF_MAX_PRIORITY>::empty() const noexcept {
-  for(unsigned i=0; i<TF_MAX_PRIORITY; i++) {
-    if(!empty(i)) {
-      return false;
-    }
-  }
-  return true;
-}
-
-// Function: empty
-template <typename T, unsigned TF_MAX_PRIORITY>
-bool TaskQueue<T, TF_MAX_PRIORITY>::empty(unsigned p) const noexcept {
-  int64_t b = _bottom[p].data.load(std::memory_order_relaxed);
-  int64_t t = _top[p].data.load(std::memory_order_relaxed);
-  return (b <= t);
-}
-
-// Function: size
-template <typename T, unsigned TF_MAX_PRIORITY>
-size_t TaskQueue<T, TF_MAX_PRIORITY>::size() const noexcept {
-  size_t s;
-  unroll<0, TF_MAX_PRIORITY, 1>([&](auto i) { s = i ? size(i) + s : size(i); });
-  return s;
-}
-
-// Function: size
-template <typename T, unsigned TF_MAX_PRIORITY>
-size_t TaskQueue<T, TF_MAX_PRIORITY>::size(unsigned p) const noexcept {
-  int64_t b = _bottom[p].data.load(std::memory_order_relaxed);
-  int64_t t = _top[p].data.load(std::memory_order_relaxed);
-  return static_cast<size_t>(b >= t ? b - t : 0);
-}
-
-// Function: push
-template <typename T, unsigned TF_MAX_PRIORITY>
-TF_FORCE_INLINE void TaskQueue<T, TF_MAX_PRIORITY>::push(T o, unsigned p) {
-
-  int64_t b = _bottom[p].data.load(std::memory_order_relaxed);
-  int64_t t = _top[p].data.load(std::memory_order_acquire);
-  Array* a = _array[p].load(std::memory_order_relaxed);
-
-  // queue is full
-  if(a->capacity() - 1 < (b - t)) {
-    a = resize_array(a, p, b, t);
-  }
-
-  a->push(b, o);
-  std::atomic_thread_fence(std::memory_order_release);
-  _bottom[p].data.store(b + 1, std::memory_order_relaxed);
-}
-
-// Function: pop
-template <typename T, unsigned TF_MAX_PRIORITY>
-T TaskQueue<T, TF_MAX_PRIORITY>::pop() {
-  for(unsigned i=0; i<TF_MAX_PRIORITY; i++) {
-    if(auto t = pop(i); t) {
-      return t;
-    }
-  }
-  return nullptr;
-}
-
-// Function: pop
-template <typename T, unsigned TF_MAX_PRIORITY>
-TF_FORCE_INLINE T TaskQueue<T, TF_MAX_PRIORITY>::pop(unsigned p) {
-
-  int64_t b = _bottom[p].data.load(std::memory_order_relaxed) - 1;
-  Array* a = _array[p].load(std::memory_order_relaxed);
-  _bottom[p].data.store(b, std::memory_order_relaxed);
-  std::atomic_thread_fence(std::memory_order_seq_cst);
-  int64_t t = _top[p].data.load(std::memory_order_relaxed);
-
-  T item {nullptr};
-
-  if(t <= b) {
-    item = a->pop(b);
-    if(t == b) {
-      // the last item just got stolen
-      if(!_top[p].data.compare_exchange_strong(t, t+1,
-                                               std::memory_order_seq_cst,
-                                               std::memory_order_relaxed)) {
-        item = nullptr;
-      }
-      _bottom[p].data.store(b + 1, std::memory_order_relaxed);
-    }
-  }
-  else {
-    _bottom[p].data.store(b + 1, std::memory_order_relaxed);
-  }
-
-  return item;
-}
-
-// Function: steal
-template <typename T, unsigned TF_MAX_PRIORITY>
-T TaskQueue<T, TF_MAX_PRIORITY>::steal() {
-  for(unsigned i=0; i<TF_MAX_PRIORITY; i++) {
-    if(auto t = steal(i); t) {
-      return t;
-    }
-  }
-  return nullptr;
-}
-
-// Function: steal
-template <typename T, unsigned TF_MAX_PRIORITY>
-T TaskQueue<T, TF_MAX_PRIORITY>::steal(unsigned p) {
-  
-  int64_t t = _top[p].data.load(std::memory_order_acquire);
-  std::atomic_thread_fence(std::memory_order_seq_cst);
-  int64_t b = _bottom[p].data.load(std::memory_order_acquire);
-
-  T item {nullptr};
-
-  if(t < b) {
-    Array* a = _array[p].load(std::memory_order_consume);
-    item = a->pop(t);
-    if(!_top[p].data.compare_exchange_strong(t, t+1,
-                                             std::memory_order_seq_cst,
-                                             std::memory_order_relaxed)) {
-      return nullptr;
-    }
-  }
-
-  return item;
-}
-
-// Function: capacity
-template <typename T, unsigned TF_MAX_PRIORITY>
-int64_t TaskQueue<T, TF_MAX_PRIORITY>::capacity() const noexcept {
-  size_t s;
-  unroll<0, TF_MAX_PRIORITY, 1>([&](auto i) { 
-    s = i ? capacity(i) + s : capacity(i); 
-  });
-  return s;
-}
-
-// Function: capacity
-template <typename T, unsigned TF_MAX_PRIORITY>
-int64_t TaskQueue<T, TF_MAX_PRIORITY>::capacity(unsigned p) const noexcept {
-  return _array[p].load(std::memory_order_relaxed)->capacity();
-}
-
-template <typename T, unsigned TF_MAX_PRIORITY>
-TF_NO_INLINE typename TaskQueue<T, TF_MAX_PRIORITY>::Array*
-  TaskQueue<T, TF_MAX_PRIORITY>::resize_array(Array* a, unsigned p, std::int64_t b, std::int64_t t) {
-
-  Array* tmp = a->resize(b, t);
-  _garbage[p].push_back(a);
-  std::swap(a, tmp);
-  _array[p].store(a, std::memory_order_release);
-  // Note: the original paper using relaxed causes t-san to complain
-  //_array.store(a, std::memory_order_relaxed);
-  return a;
-}
-
-
-}  // end of namespace tf -----------------------------------------------------
diff --git a/bundled/taskflow-3.7.0/taskflow/core/worker.hpp b/bundled/taskflow-3.7.0/taskflow/core/worker.hpp
deleted file mode 100644 (file)
index 8f86381..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-#pragma once
-
-#include "declarations.hpp"
-#include "tsq.hpp"
-#include "notifier.hpp"
-
-/**
-@file worker.hpp
-@brief worker include file
-*/
-
-namespace tf {
-
-// ----------------------------------------------------------------------------
-// Class Definition: Worker
-// ----------------------------------------------------------------------------
-
-/**
-@class Worker
-
-@brief class to create a worker in an executor
-
-The class is primarily used by the executor to perform work-stealing algorithm.
-Users can access a worker object and alter its property
-(e.g., changing the thread affinity in a POSIX-like system)
-using tf::WorkerInterface.
-*/
-class Worker {
-
-  friend class Executor;
-  friend class WorkerView;
-
-  public:
-
-    /**
-    @brief queries the worker id associated with its parent executor
-
-    A worker id is a unsigned integer in the range <tt>[0, N)</tt>,
-    where @c N is the number of workers spawned at the construction
-    time of the executor.
-    */
-    inline size_t id() const { return _id; }
-
-    /**
-    @brief acquires a pointer access to the underlying thread
-    */
-    inline std::thread* thread() const { return _thread; }
-
-    /**
-    @brief queries the size of the queue (i.e., number of enqueued tasks to
-           run) associated with the worker
-    */
-    inline size_t queue_size() const { return _wsq.size(); }
-    
-    /**
-    @brief queries the current capacity of the queue
-    */
-    inline size_t queue_capacity() const { return static_cast<size_t>(_wsq.capacity()); }
-
-  private:
-
-    size_t _id;
-    size_t _vtm;
-    Executor* _executor;
-    std::thread* _thread;
-    Notifier::Waiter* _waiter;
-    std::default_random_engine _rdgen { std::random_device{}() };
-    TaskQueue<Node*> _wsq;
-    Node* _cache;
-};
-
-// ----------------------------------------------------------------------------
-// Class Definition: PerThreadWorker
-// ----------------------------------------------------------------------------
-
-/**
-@private
-*/
-//struct PerThreadWorker {
-//
-//  Worker* worker;
-//
-//  PerThreadWorker() : worker {nullptr} {}
-//
-//  PerThreadWorker(const PerThreadWorker&) = delete;
-//  PerThreadWorker(PerThreadWorker&&) = delete;
-//
-//  PerThreadWorker& operator = (const PerThreadWorker&) = delete;
-//  PerThreadWorker& operator = (PerThreadWorker&&) = delete;
-//};
-
-/**
-@private
-*/
-//inline PerThreadWorker& this_worker() {
-//  thread_local PerThreadWorker worker;
-//  return worker;
-//}
-
-
-// ----------------------------------------------------------------------------
-// Class Definition: WorkerView
-// ----------------------------------------------------------------------------
-
-/**
-@class WorkerView
-
-@brief class to create an immutable view of a worker in an executor
-
-An executor keeps a set of internal worker threads to run tasks.
-A worker view provides users an immutable interface to observe
-when a worker runs a task, and the view object is only accessible
-from an observer derived from tf::ObserverInterface.
-*/
-class WorkerView {
-
-  friend class Executor;
-
-  public:
-
-    /**
-    @brief queries the worker id associated with its parent executor
-
-    A worker id is a unsigned integer in the range <tt>[0, N)</tt>,
-    where @c N is the number of workers spawned at the construction
-    time of the executor.
-    */
-    size_t id() const;
-
-    /**
-    @brief queries the size of the queue (i.e., number of pending tasks to
-           run) associated with the worker
-    */
-    size_t queue_size() const;
-
-    /**
-    @brief queries the current capacity of the queue
-    */
-    size_t queue_capacity() const;
-
-  private:
-
-    WorkerView(const Worker&);
-    WorkerView(const WorkerView&) = default;
-
-    const Worker& _worker;
-
-};
-
-// Constructor
-inline WorkerView::WorkerView(const Worker& w) : _worker{w} {
-}
-
-// function: id
-inline size_t WorkerView::id() const {
-  return _worker._id;
-}
-
-// Function: queue_size
-inline size_t WorkerView::queue_size() const {
-  return _worker._wsq.size();
-}
-
-// Function: queue_capacity
-inline size_t WorkerView::queue_capacity() const {
-  return static_cast<size_t>(_worker._wsq.capacity());
-}
-
-
-}  // end of namespact tf -----------------------------------------------------
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/taskflow.hpp b/bundled/taskflow-3.7.0/taskflow/taskflow.hpp
deleted file mode 100644 (file)
index c2403f8..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-#pragma once
-
-#include "core/executor.hpp"
-#include "core/async.hpp"
-#include "algorithm/critical.hpp"
-
-/**
-@dir taskflow
-@brief root taskflow include dir
-*/
-
-/**
-@dir taskflow/core
-@brief taskflow core include dir
-*/
-
-/**
-@dir taskflow/algorithm
-@brief taskflow algorithms include dir
-*/
-
-/**
-@dir taskflow/cuda
-@brief taskflow CUDA include dir
-*/
-
-/**
-@file taskflow/taskflow.hpp
-@brief main taskflow include file
-*/
-
-// TF_VERSION % 100 is the patch level
-// TF_VERSION / 100 % 1000 is the minor version
-// TF_VERSION / 100000 is the major version
-
-// current version: 3.7.0
-#define TF_VERSION 300700
-
-#define TF_MAJOR_VERSION TF_VERSION/100000
-#define TF_MINOR_VERSION TF_VERSION/100%1000
-#define TF_PATCH_VERSION TF_VERSION%100
-
-/**
-@brief taskflow namespace
-*/
-namespace tf {
-
-/**
-@private
-*/
-namespace detail { }
-
-
-/**
-@brief queries the version information in a string format @c major.minor.patch
-
-Release notes are available here: https://taskflow.github.io/taskflow/Releases.html
-*/
-constexpr const char* version() {
-  return "3.7.0";
-}
-
-
-}  // end of namespace tf -----------------------------------------------------
-
-
-
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/utility/iterator.hpp b/bundled/taskflow-3.7.0/taskflow/utility/iterator.hpp
deleted file mode 100644 (file)
index 8636a3b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <type_traits>
-
-namespace tf {
-
-template <typename T>
-constexpr std::enable_if_t<std::is_integral<std::decay_t<T>>::value, bool>
-is_range_invalid(T beg, T end, T step) {
-  return ((step == 0 && beg != end) ||
-          (beg < end && step <=  0) ||  // positive range
-          (beg > end && step >=  0));   // negative range
-}
-
-template <typename T>
-constexpr std::enable_if_t<std::is_integral<std::decay_t<T>>::value, size_t>
-distance(T beg, T end, T step) {
-  return (end - beg + step + (step > 0 ? -1 : 1)) / step;
-}
-
-}  // end of namespace tf -----------------------------------------------------
diff --git a/bundled/taskflow-3.7.0/taskflow/utility/math.hpp b/bundled/taskflow-3.7.0/taskflow/utility/math.hpp
deleted file mode 100644 (file)
index f80053e..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-#pragma once
-
-#include <atomic>
-
-namespace tf {
-
-// rounds the given 64-bit unsigned integer to the nearest power of 2
-template <typename T, std::enable_if_t<
-  (std::is_unsigned_v<std::decay_t<T>> && sizeof(T) == 8) , void
->* = nullptr>
-constexpr T next_pow2(T x) {
-  if(x == 0) return 1;
-  x--;
-  x |= x>>1;
-       x |= x>>2;
-       x |= x>>4;
-       x |= x>>8;
-       x |= x>>16;
-       x |= x>>32;
-  x++;
-  return x;
-}
-
-// rounds the given 32-bit unsigned integer to the nearest power of 2
-template <typename T, std::enable_if_t<
-  (std::is_unsigned_v<std::decay_t<T>> && sizeof(T) == 4), void
->* = nullptr>
-constexpr T next_pow2(T x) {
-  if(x == 0) return 1;
-  x--;
-  x |= x>>1;
-       x |= x>>2;
-       x |= x>>4;
-       x |= x>>8;
-       x |= x>>16;
-  x++;
-  return x;
-}
-
-// checks if the given number if a power of 2
-template <typename T, std::enable_if_t<
-  std::is_integral_v<std::decay_t<T>>, void>* = nullptr
->
-constexpr bool is_pow2(const T& x) {
-  return x && (!(x&(x-1)));
-}
-
-//// finds the ceil of x divided by b
-//template <typename T, std::enable_if_t<
-//  std::is_integral_v<std::decay_t<T>>, void>* = nullptr
-//>
-//constexpr T ceil(const T& x, const T& y) {
-//  //return (x + y - 1) / y;
-//  return (x-1) / y + 1;
-//}
-
-/**
-@brief returns floor(log2(n)), assumes n > 0
-*/
-template<typename T>
-constexpr int log2(T n) {
-  int log = 0;
-  while (n >>= 1) {
-    ++log;
-  }
-  return log;
-}
-
-/**
-@brief finds the median of three numbers of dereferenced iterators using
-       the given comparator
-*/
-template <typename RandItr, typename C>
-RandItr median_of_three(RandItr l, RandItr m, RandItr r, C cmp) {
-  return cmp(*l, *m) ? (cmp(*m, *r) ? m : (cmp(*l, *r) ? r : l ))
-                     : (cmp(*r, *m) ? m : (cmp(*r, *l) ? r : l ));
-}
-
-/**
-@brief finds the pseudo median of a range of items using spreaded
-       nine numbers
- */
-template <typename RandItr, typename C>
-RandItr pseudo_median_of_nine(RandItr beg, RandItr end, C cmp) {
-  size_t N = std::distance(beg, end);
-  size_t offset = N >> 3;
-  return median_of_three(
-    median_of_three(beg, beg+offset, beg+(offset*2), cmp),
-    median_of_three(beg+(offset*3), beg+(offset*4), beg+(offset*5), cmp),
-    median_of_three(beg+(offset*6), beg+(offset*7), end-1, cmp),
-    cmp
-  );
-}
-
-/**
-@brief sorts two elements of dereferenced iterators using the given
-       comparison function
-*/
-template<typename Iter, typename Compare>
-void sort2(Iter a, Iter b, Compare comp) {
-  if (comp(*b, *a)) std::iter_swap(a, b);
-}
-
-/**
-@brief sorts three elements of dereferenced iterators using the given
-       comparison function
-*/
-template<typename Iter, typename Compare>
-void sort3(Iter a, Iter b, Iter c, Compare comp) {
-  sort2(a, b, comp);
-  sort2(b, c, comp);
-  sort2(a, b, comp);
-}
-
-/**
-@brief generates a program-wise unique id of the give type (thread-safe)
-*/
-template <typename T, std::enable_if_t<std::is_integral_v<T>, void>* = nullptr>
-T unique_id() {
-  static std::atomic<T> counter{0};
-  return counter.fetch_add(1, std::memory_order_relaxed);
-}
-
-/**
-@brief updates an atomic variable with a maximum value
-*/
-template <typename T>
-inline void atomic_max(std::atomic<T>& v, const T& max_v) noexcept {
-  T prev = v.load(std::memory_order_relaxed);
-  while(prev < max_v && 
-        !v.compare_exchange_weak(prev, max_v, std::memory_order_relaxed,
-                                              std::memory_order_relaxed)) {
-  }
-}
-
-/**
-@brief updates an atomic variable with a minimum value
-*/
-template <typename T>
-inline void atomic_min(std::atomic<T>& v, const T& min_v) noexcept {
-  T prev = v.load(std::memory_order_relaxed);
-  while(prev > min_v && 
-        !v.compare_exchange_weak(prev, min_v, std::memory_order_relaxed,
-                                              std::memory_order_relaxed)) {
-  }
-}
-
-}  // end of namespace tf -----------------------------------------------------
-
-
-
diff --git a/bundled/taskflow-3.7.0/taskflow/utility/os.hpp b/bundled/taskflow-3.7.0/taskflow/utility/os.hpp
deleted file mode 100644 (file)
index 23ac301..0000000
+++ /dev/null
@@ -1,196 +0,0 @@
-#pragma once
-
-#include <cstdlib>
-#include <cstdio>
-#include <string>
-
-#define TF_OS_LINUX 0
-#define TF_OS_DRAGONFLY 0
-#define TF_OS_FREEBSD 0
-#define TF_OS_NETBSD 0
-#define TF_OS_OPENBSD 0
-#define TF_OS_DARWIN 0
-#define TF_OS_WINDOWS 0
-#define TF_OS_CNK 0
-#define TF_OS_HURD 0
-#define TF_OS_SOLARIS 0
-#define TF_OS_UNIX 0
-
-#ifdef _WIN32
-#undef TF_OS_WINDOWS
-#define TF_OS_WINDOWS 1
-#endif
-
-#ifdef __CYGWIN__
-#undef TF_OS_WINDOWS
-#define TF_OS_WINDOWS 1
-#endif
-
-#if (defined __APPLE__ && defined __MACH__)
-#undef TF_OS_DARWIN
-#define TF_OS_DARWIN 1
-#endif
-
-// in some ppc64 linux installations, only the second condition is met
-#if (defined __linux)
-#undef TF_OS_LINUX
-#define TF_OS_LINUX 1
-#elif (defined __linux__)
-#undef TF_OS_LINUX
-#define TF_OS_LINUX 1
-#else
-#endif
-
-#if (defined __DragonFly__)
-#undef TF_OS_DRAGONFLY
-#define TF_OS_DRAGONFLY 1
-#endif
-
-#if (defined __FreeBSD__)
-#undef TF_OS_FREEBSD
-#define TF_OS_FREEBSD 1
-#endif
-
-#if (defined __NetBSD__)
-#undef TF_OS_NETBSD
-#define TF_OS_NETBSD 1
-#endif
-
-#if (defined __OpenBSD__)
-#undef TF_OS_OPENBSD
-#define TF_OS_OPENBSD 1
-#endif
-
-#if (defined __bgq__)
-#undef TF_OS_CNK
-#define TF_OS_CNK 1
-#endif
-
-#if (defined __GNU__)
-#undef TF_OS_HURD
-#define TF_OS_HURD 1
-#endif
-
-#if (defined __sun)
-#undef TF_OS_SOLARIS
-#define TF_OS_SOLARIS 1
-#endif
-
-#if (1 !=                                                                  \
-     TF_OS_LINUX + TF_OS_DRAGONFLY + TF_OS_FREEBSD + TF_OS_NETBSD +        \
-     TF_OS_OPENBSD + TF_OS_DARWIN + TF_OS_WINDOWS + TF_OS_HURD +           \
-     TF_OS_SOLARIS)
-#define TF_OS_UNKNOWN 1
-#endif
-
-#if TF_OS_LINUX || TF_OS_DRAGONFLY || TF_OS_FREEBSD || TF_OS_NETBSD ||     \
-    TF_OS_OPENBSD || TF_OS_DARWIN || TF_OS_HURD || TF_OS_SOLARIS
-#undef TF_OS_UNIX
-#define TF_OS_UNIX 1
-#endif
-
-
-//-----------------------------------------------------------------------------
-// Cache line alignment
-//-----------------------------------------------------------------------------
-#if defined(__i386__) || defined(__x86_64__)
-  #define TF_CACHELINE_SIZE 64
-#elif defined(__powerpc64__)
-  // TODO
-  // This is the L1 D-cache line size of our Power7 machines.
-  // Need to check if this is appropriate for other PowerPC64 systems.
-  #define TF_CACHELINE_SIZE 128
-#elif defined(__arm__)
-  // Cache line sizes for ARM: These values are not strictly correct since
-  // cache line sizes depend on implementations, not architectures.
-  // There are even implementations with cache line sizes configurable
-  // at boot time.
-  #if defined(__ARM_ARCH_5T__)
-    #define TF_CACHELINE_SIZE 32
-  #elif defined(__ARM_ARCH_7A__)
-    #define TF_CACHELINE_SIZE 64
-  #endif
-#endif
-
-#ifndef TF_CACHELINE_SIZE
-// A reasonable default guess.  Note that overestimates tend to waste more
-// space, while underestimates tend to waste more time.
-  #define TF_CACHELINE_SIZE 64
-#endif
-
-
-
-//-----------------------------------------------------------------------------
-// pause
-//-----------------------------------------------------------------------------
-//#if __has_include (<immintrin.h>)
-//  #define TF_HAS_MM_PAUSE 1
-//  #include <immintrin.h>
-//#endif
-
-namespace tf {
-
-// Struct: CachelineAligned
-// Due to prefetch, we typically do 2x cacheline for the alignment.
-template <typename T>
-struct CachelineAligned {
-  alignas (2*TF_CACHELINE_SIZE) T data;
-};
-
-// Function: get_env
-inline std::string get_env(const std::string& str) {
-#ifdef _MSC_VER
-  char *ptr = nullptr;
-  size_t len = 0;
-
-  if(_dupenv_s(&ptr, &len, str.c_str()) == 0 && ptr != nullptr) {
-    std::string res(ptr, len);
-    std::free(ptr);
-    return res;
-  }
-  return "";
-
-#else
-  auto ptr = std::getenv(str.c_str());
-  return ptr ? ptr : "";
-#endif
-}
-
-// Function: has_env
-inline bool has_env(const std::string& str) {
-#ifdef _MSC_VER
-  char *ptr = nullptr;
-  size_t len = 0;
-
-  if(_dupenv_s(&ptr, &len, str.c_str()) == 0 && ptr != nullptr) {
-    std::string res(ptr, len);
-    std::free(ptr);
-    return true;
-  }
-  return false;
-
-#else
-  auto ptr = std::getenv(str.c_str());
-  return ptr ? true : false;
-#endif
-}
-
-// Procedure: relax_cpu
-//inline void relax_cpu() {
-//#ifdef TF_HAS_MM_PAUSE
-//  _mm_pause();
-//#endif
-//}
-
-
-
-}  // end of namespace tf -----------------------------------------------------
-
-
-
-
-
-
-
-
-
index 9e55cc72886e12d597a0eb2c52dba55d026b5f3e..fee8811229d2f2aef6746da7f49838d21c03c66d 100644 (file)
@@ -26,19 +26,19 @@ macro(feature_taskflow_find_external var)
     set(${var} TRUE)
   endif()
 
-  if(TASKFLOW_VERSION VERSION_LESS "3.7")
+  if(TASKFLOW_VERSION VERSION_LESS "3.10")
     # Clear the previously determined version numbers to avoid confusion
     set(TASKFLOW_VERSION "bundled")
     set(TASKFLOW_VERSION_MAJOR "")
     set(TASKFLOW_VERSION_MINOR "")
 
     message(STATUS
-      "The externally provided Taskflow library is older than version 3.7, "
+      "The externally provided Taskflow library is older than version 3.10, "
       "which cannot be used with deal.II."
       )
     set(TASKFLOW_ADDITIONAL_ERROR_STRING
       "The externally provided Taskflow library is older than version\n"
-      "3.7, which is the oldest version compatible with deal.II."
+      "3.10, which is the oldest version compatible with deal.II."
       )
     set(${var} FALSE)
   endif()

In the beginning the Universe was created. This has made a lot of people very angry and has been widely regarded as a bad move.

Douglas Adams


Typeset in Trocchi and Trocchi Bold Sans Serif.