From c58a252ab1ba55641072d00ff806e050ba786788 Mon Sep 17 00:00:00 2001 From: Tsung-Wei Huang Date: Sat, 27 Jun 2026 22:19:11 -0500 Subject: [PATCH 1/2] added emplace overloads for module tasking --- doxygen/releases/release-4.2.0.dox | 4 + taskflow/core/flow_builder.hpp | 70 +++++++++++ unittests/test_modules.cpp | 181 +++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) diff --git a/doxygen/releases/release-4.2.0.dox b/doxygen/releases/release-4.2.0.dox index 16aa75373..5ef735e05 100644 --- a/doxygen/releases/release-4.2.0.dox +++ b/doxygen/releases/release-4.2.0.dox @@ -37,6 +37,10 @@ If your project does not support C++20, please use %Taskflow v3.11.0, which can @subsection release-4-2-0_taskflow_core Taskflow Core ++ added two tf::Taskflow::emplace overloads for module tasks + + tf::Taskflow::emplace(tf::GraphLike) equivalent to tf::Taskflow::composed_of + + tf::Taskflow::emplace(tf::Graph&&) equivalent to tf::Taskflow::adopt + @subsection release-4-2-0_utilities Utilities @section release-4-2-0_bug_fixes Bug Fixes diff --git a/taskflow/core/flow_builder.hpp b/taskflow/core/flow_builder.hpp index 1c81836ec..cdf725e36 100644 --- a/taskflow/core/flow_builder.hpp +++ b/taskflow/core/flow_builder.hpp @@ -302,6 +302,65 @@ class FlowBuilder { */ Task adopt(Graph&& graph); + /** + @brief creates a module task for the target object (convenience overload of tf::FlowBuilder::composed_of) + + @tparam T type satisfying tf::GraphLike + + @param object a custom object that defines the method @c T::graph() + + @return a tf::Task handle + + This overload lets you create a module task through the same @c emplace + call you already use for static, runtime, subflow, and condition tasks, + instead of reaching for the differently-named @c composed_of. + It is equivalent to calling tf::FlowBuilder::composed_of(object) and + references the externally-owned graph of @c object, so the caller remains + responsible for keeping @c object alive for as long as the resulting task + may run. + + @code{.cpp} + tf::Taskflow t1, t2; + t1.emplace([](){ std::cout << "t1"; }); + + // equivalent to: tf::Task comp = t2.composed_of(t1); + tf::Task comp = t2.emplace(t1); + @endcode + + @note + Please refer to @ref ComposableTasking for details. + */ + template + Task emplace(T& object); + + /** + @brief creates a module task from a graph by taking over its ownership + (convenience overload of tf::FlowBuilder::adopt) + + @param graph the graph to adopt (moved into the task) + + @return a Task handle to the adopted module task + + This overload lets you create an adopted module task through the same + @c emplace call you already use for other task types, instead of reaching + for the differently-named @c adopt. + It is equivalent to calling tf::FlowBuilder::adopt(std::move(graph)) and + transfers ownership of @c graph into the task; the caller has no access + to the moved-from graph afterward. + + @code{.cpp} + tf::Taskflow taskflow; + tf::Graph g; + tf::FlowBuilder{g}.emplace([]{ std::cout << "task in adopted graph\n"; }); + + // equivalent to: taskflow.adopt(std::move(g)).name("adopted"); + taskflow.emplace(std::move(g)).name("adopted"); + @endcode + + @note Please refer to @ref ComposableTasking for details. + */ + Task emplace(Graph&& graph); + /** @brief creates a placeholder task @@ -1631,6 +1690,17 @@ inline Task FlowBuilder::adopt(Graph&& graph) { )); } +// Function: emplace (convenience overload of composed_of) +template +Task FlowBuilder::emplace(T& object) { + return composed_of(object); +} + +// Function: emplace (convenience overload of adopt) +inline Task FlowBuilder::emplace(Graph&& graph) { + return adopt(std::move(graph)); +} + // Function: placeholder inline Task FlowBuilder::placeholder() { auto node = _graph._emplace_back(NSTATE::NONE, ESTATE::NONE, DefaultTaskParams{}, nullptr, nullptr, 0, diff --git a/unittests/test_modules.cpp b/unittests/test_modules.cpp index 8170b2c91..e2365297d 100644 --- a/unittests/test_modules.cpp +++ b/unittests/test_modules.cpp @@ -820,3 +820,184 @@ TEST_CASE("AdoptedModule.7threads" * doctest::timeout(300)) { TEST_CASE("AdoptedModule.8threads" * doctest::timeout(300)) { adopted_module(8); } + +// -------------------------------------------------------- +// Testcase: EmplaceModule (emplace as a convenience overload of composed_of) +// -------------------------------------------------------- +void emplace_module(unsigned W) { + + tf::Executor executor(W); + + tf::Taskflow f0; + + int cnt {0}; + + auto A = f0.emplace([&cnt](){ ++cnt; }); + auto B = f0.emplace([&cnt](){ ++cnt; }); + auto C = f0.emplace([&cnt](){ ++cnt; }); + auto D = f0.emplace([&cnt](){ ++cnt; }); + auto E = f0.emplace([&cnt](){ ++cnt; }); + + A.precede(B); + B.precede(C); + C.precede(D); + D.precede(E); + + tf::Taskflow f1; + + // module 1 + std::tie(A, B, C, D, E) = f1.emplace( + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; } + ); + A.precede(B); + B.precede(C); + C.precede(D); + D.precede(E); + + // emplace(f0) should behave identically to composed_of(f0): f0 is + // referenced, not consumed, so it can be composed into f1 multiple times. + auto m1_1 = f1.emplace(f0); + E.precede(m1_1); + + executor.run(f1).get(); + REQUIRE(cnt == 10); + + cnt = 0; + executor.run_n(f1, 100).get(); + REQUIRE(cnt == 10 * 100); + + auto m1_2 = f1.emplace(f0); + m1_1.precede(m1_2); + + for(int n=0; n<100; n++) { + cnt = 0; + executor.run_n(f1, n).get(); + REQUIRE(cnt == 15*n); + } + + cnt = 0; + for(int n=0; n<100; n++) { + executor.run(f1); + } + + executor.wait_for_all(); + + REQUIRE(cnt == 1500); +} + +TEST_CASE("EmplaceModule.1thread" * doctest::timeout(300)) { + emplace_module(1); +} + +TEST_CASE("EmplaceModule.2threads" * doctest::timeout(300)) { + emplace_module(2); +} + +TEST_CASE("EmplaceModule.3threads" * doctest::timeout(300)) { + emplace_module(3); +} + +TEST_CASE("EmplaceModule.4threads" * doctest::timeout(300)) { + emplace_module(4); +} + +TEST_CASE("EmplaceModule.5threads" * doctest::timeout(300)) { + emplace_module(5); +} + +TEST_CASE("EmplaceModule.6threads" * doctest::timeout(300)) { + emplace_module(6); +} + +TEST_CASE("EmplaceModule.7threads" * doctest::timeout(300)) { + emplace_module(7); +} + +TEST_CASE("EmplaceModule.8threads" * doctest::timeout(300)) { + emplace_module(8); +} + +// -------------------------------------------------------- +// Testcase: EmplaceAdoptedModule (emplace as a convenience overload of adopt) +// -------------------------------------------------------- +void emplace_adopted_module(unsigned W) { + + tf::Executor executor(W); + + tf::Taskflow f0; + + int cnt {0}; + + auto A = f0.emplace([&cnt](){ ++cnt; }); + auto B = f0.emplace([&cnt](){ ++cnt; }); + auto C = f0.emplace([&cnt](){ ++cnt; }); + auto D = f0.emplace([&cnt](){ ++cnt; }); + auto E = f0.emplace([&cnt](){ ++cnt; }); + + A.precede(B); + B.precede(C); + C.precede(D); + D.precede(E); + + tf::Taskflow f1; + + // module 1 + std::tie(A, B, C, D, E) = f1.emplace( + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; }, + [&cnt] () { ++cnt; } + ); + A.precede(B); + B.precede(C); + C.precede(D); + D.precede(E); + + // emplace(std::move(graph)) should behave identically to + // adopt(std::move(graph)): ownership of f0's graph transfers into f1, + // leaving f0's graph empty. + auto m1_1 = f1.emplace(std::move(f0.graph())); + E.precede(m1_1); + + REQUIRE(f0.graph().empty()); + + executor.run(f1).get(); + REQUIRE(cnt == 10); +} + +TEST_CASE("EmplaceAdoptedModule.1thread" * doctest::timeout(300)) { + emplace_adopted_module(1); +} + +TEST_CASE("EmplaceAdoptedModule.2threads" * doctest::timeout(300)) { + emplace_adopted_module(2); +} + +TEST_CASE("EmplaceAdoptedModule.3threads" * doctest::timeout(300)) { + emplace_adopted_module(3); +} + +TEST_CASE("EmplaceAdoptedModule.4threads" * doctest::timeout(300)) { + emplace_adopted_module(4); +} + +TEST_CASE("EmplaceAdoptedModule.5threads" * doctest::timeout(300)) { + emplace_adopted_module(5); +} + +TEST_CASE("EmplaceAdoptedModule.6threads" * doctest::timeout(300)) { + emplace_adopted_module(6); +} + +TEST_CASE("EmplaceAdoptedModule.7threads" * doctest::timeout(300)) { + emplace_adopted_module(7); +} + +TEST_CASE("EmplaceAdoptedModule.8threads" * doctest::timeout(300)) { + emplace_adopted_module(8); +} From 6086074147ac2a300f1ac62220bd2e7f764b6eb4 Mon Sep 17 00:00:00 2001 From: Tsung-Wei Huang Date: Wed, 1 Jul 2026 22:25:22 -0500 Subject: [PATCH 2/2] replaced generic template type with std::input_iterator in async task --- doxygen/releases/release-4.2.0.dox | 1 + taskflow/core/async.hpp | 20 +-- taskflow/core/async_task.hpp | 265 +++++++++++++++-------------- taskflow/core/executor.hpp | 44 +++-- taskflow/core/graph.hpp | 4 +- taskflow/core/runtime.hpp | 28 ++- taskflow/core/task_group.hpp | 28 ++- 7 files changed, 182 insertions(+), 208 deletions(-) diff --git a/doxygen/releases/release-4.2.0.dox b/doxygen/releases/release-4.2.0.dox index 5ef735e05..3a148249f 100644 --- a/doxygen/releases/release-4.2.0.dox +++ b/doxygen/releases/release-4.2.0.dox @@ -40,6 +40,7 @@ If your project does not support C++20, please use %Taskflow v3.11.0, which can + added two tf::Taskflow::emplace overloads for module tasks + tf::Taskflow::emplace(tf::GraphLike) equivalent to tf::Taskflow::composed_of + tf::Taskflow::emplace(tf::Graph&&) equivalent to tf::Taskflow::adopt ++ replaced generic template type in dependent async tasks with std::input_iterator @subsection release-4-2-0_utilities Utilities diff --git a/taskflow/core/async.hpp b/taskflow/core/async.hpp index fab729866..da9975af9 100644 --- a/taskflow/core/async.hpp +++ b/taskflow/core/async.hpp @@ -26,7 +26,7 @@ void Executor::_schedule_async_task(ArgsT&&... args) { } // Procedure: _schedule_dependent_async_task -template +template AsyncTask Executor::_schedule_dependent_async_task(I first, I last, size_t num_predecessors, ArgsT&&... args) { // We need to create an async-task first to acquire an ownership. @@ -221,15 +221,13 @@ tf::AsyncTask Executor::silent_dependent_async( } // Function: silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template tf::AsyncTask Executor::silent_dependent_async(F&& func, I first, I last) { return silent_dependent_async(DefaultTaskParams{}, std::forward(func), first, last); } // Function: silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template tf::AsyncTask Executor::silent_dependent_async( P&& params, F&& func, I first, I last ) { @@ -240,8 +238,7 @@ tf::AsyncTask Executor::silent_dependent_async( } // Function: _silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto Executor::_silent_dependent_async( P&& params, F&& func, I first, I last, Topology* tpg, NodeBase* parent ) { @@ -274,23 +271,20 @@ auto Executor::dependent_async(P&& params, F&& func, Tasks&&... tasks) { } // Function: dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto Executor::dependent_async(F&& func, I first, I last) { return dependent_async(DefaultTaskParams{}, std::forward(func), first, last); } // Function: dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto Executor::dependent_async(P&& params, F&& func, I first, I last) { _increment_topology(); return _dependent_async(std::forward

(params), std::forward(func), first, last, nullptr, nullptr); } // Function: _dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto Executor::_dependent_async(P&& params, F&& func, I first, I last, Topology* tpg, NodeBase* parent) { size_t num_predecessors = std::distance(first, last); diff --git a/taskflow/core/async_task.hpp b/taskflow/core/async_task.hpp index 64040515e..0d1172ab2 100644 --- a/taskflow/core/async_task.hpp +++ b/taskflow/core/async_task.hpp @@ -48,150 +48,150 @@ class AsyncTask { public: - /** - @brief constructs an empty task handle - */ - AsyncTask() = default; - - /** - @brief destroys the managed dependent-async task if this is the last owner - */ - ~AsyncTask(); - - /** - @brief constructs a dependent-async task that shares ownership of @c rhs - */ - AsyncTask(const AsyncTask& rhs); - - /** - @brief move-constructs an dependent-async task from @c rhs - */ - AsyncTask(AsyncTask&& rhs); - - /** - @brief copy-assigns the dependent-async task from @c rhs + /** + @brief constructs an empty task handle + */ + AsyncTask() = default; + + /** + @brief destroys the managed dependent-async task if this is the last owner + */ + ~AsyncTask(); + + /** + @brief constructs a dependent-async task that shares ownership of @c rhs + */ + AsyncTask(const AsyncTask& rhs); + + /** + @brief move-constructs an dependent-async task from @c rhs + */ + AsyncTask(AsyncTask&& rhs); + + /** + @brief copy-assigns the dependent-async task from @c rhs - Releases the managed object of @c this and retains a new shared ownership - of @c rhs. - */ - AsyncTask& operator = (const AsyncTask& rhs); + Releases the managed object of @c this and retains a new shared ownership + of @c rhs. + */ + AsyncTask& operator = (const AsyncTask& rhs); - /** - @brief move-assigns the dependent-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 this dependent-async task is associated with any task + /** + @brief move-assigns the dependent-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 this dependent-async task is associated with any task - An empty dependent-async task is not associated with any task created - from the executor. + An empty dependent-async task is not associated with any task created + from the executor. - @code{.cpp} - tf::AsyncTask task; - assert(task.empty()); - @endcode - */ - bool empty() const; + @code{.cpp} + tf::AsyncTask task; + assert(task.empty()); + @endcode + */ + bool empty() const; - /** - @brief release the managed object of @c this - - Releases the ownership of the managed task, if any. - After the call `*this` manages no task. - - @code{.cpp} - tf::AsyncTask task = executor.silent_dependent_async([](){}); - assert(task.empty() == false); - task.reset(); - assert(task.empty() == true); - @endcode - */ - void reset(); - - /** - @brief obtains the hashed value of this dependent-async task - - @code{.cpp} - tf::AsyncTask task = executor.silent_dependent_async([](){}); - std::cout << task.hash_value() << '\n'; - @endcode - */ - size_t hash_value() const; - - /** - @brief returns the number of shared owners that are currently managing - this dependent-async task - - In a multithreaded environment, `use_count` atomically retrieves - (with `memory_order_relaxed` load) the number of tf::AsyncTask instances that manage - the current task. - - @code{.cpp} - tf::AsyncTask task; - assert(task.use_count() == 0); - @endcode - */ - size_t use_count() const; - - /** - @brief checks if this dependent-async task finishes - - In a multithreaded environment, `is_done` atomically retrieves - (with `memory_order_acquire` load) the underlying state bit that indicates - the completion of this dependent-async task. - If the dependent-async task is empty, returns `true`. - - @code{.cpp} - tf::AsyncTask task = executor.silent_dependent_async([](){}); - while(task.is_done() == false); - std::cout << "dependent-async task finishes\n"; - - task.reset(); - assert(task.is_done() == true); - @endcode - - */ - bool is_done() const; - - /** - @brief retrieves the exception pointer of this task - - This method retrieves the exception pointer of the task, if any, that was silently caught by the executor. - For example, the code below retrieves the exception pointer of a dependent-async task that does not have - a shared state to propagate its exception. - - @code{.cpp} - tf::AsyncTask task = executor.silent_dependent_async([&](){ - throw std::runtime_error("oops"); - }); - executor.wait_for_all(); - - // propagate the exception to the outer caller - assert(task.exception_ptr() != nullptr); - std::rethrow_exception(task.exception_ptr()); - @endcode + /** + @brief release the managed object of @c this + + Releases the ownership of the managed task, if any. + After the call `*this` manages no task. + + @code{.cpp} + tf::AsyncTask task = executor.silent_dependent_async([](){}); + assert(task.empty() == false); + task.reset(); + assert(task.empty() == true); + @endcode + */ + void reset(); + + /** + @brief obtains the hashed value of this dependent-async task + + @code{.cpp} + tf::AsyncTask task = executor.silent_dependent_async([](){}); + std::cout << task.hash_value() << '\n'; + @endcode + */ + size_t hash_value() const; + + /** + @brief returns the number of shared owners that are currently managing + this dependent-async task + + In a multithreaded environment, `use_count` atomically retrieves + (with `memory_order_relaxed` load) the number of tf::AsyncTask instances that manage + the current task. + + @code{.cpp} + tf::AsyncTask task; + assert(task.use_count() == 0); + @endcode + */ + size_t use_count() const; + + /** + @brief checks if this dependent-async task finishes + + In a multithreaded environment, `is_done` atomically retrieves + (with `memory_order_acquire` load) the underlying state bit that indicates + the completion of this dependent-async task. + If the dependent-async task is empty, returns `true`. + + @code{.cpp} + tf::AsyncTask task = executor.silent_dependent_async([](){}); + while(task.is_done() == false); + std::cout << "dependent-async task finishes\n"; + + task.reset(); + assert(task.is_done() == true); + @endcode + + */ + bool is_done() const; + + /** + @brief retrieves the exception pointer of this task + + This method retrieves the exception pointer of the task, if any, that was silently caught by the executor. + For example, the code below retrieves the exception pointer of a dependent-async task that does not have + a shared state to propagate its exception. + + @code{.cpp} + tf::AsyncTask task = executor.silent_dependent_async([&](){ + throw std::runtime_error("oops"); + }); + executor.wait_for_all(); + + // propagate the exception to the outer caller + assert(task.exception_ptr() != nullptr); + std::rethrow_exception(task.exception_ptr()); + @endcode - */ - std::exception_ptr exception_ptr() const; - - /** - @brief queries if the task has an exception pointer + */ + std::exception_ptr exception_ptr() const; + + /** + @brief queries if the task has an exception pointer - The method checks whether the task holds a pointer to a silently caught exception. - */ - bool has_exception_ptr() const; + The method checks whether the task holds a pointer to a silently caught exception. + */ + bool has_exception_ptr() const; private: - explicit AsyncTask(Node*); + explicit AsyncTask(Node*); - Node* _node {nullptr}; + Node* _node {nullptr}; - void _incref(); - void _decref(); + void _incref(); + void _decref(); }; // Constructor @@ -293,4 +293,5 @@ inline bool AsyncTask::is_done() const { return _node == nullptr ? true: (_node->_estate.load(std::memory_order_acquire) & ESTATE::FINISHED); } + } // end of namespace tf ---------------------------------------------------- diff --git a/taskflow/core/executor.hpp b/taskflow/core/executor.hpp index 38f3d28d0..10d4d7316 100644 --- a/taskflow/core/executor.hpp +++ b/taskflow/core/executor.hpp @@ -754,7 +754,7 @@ class Executor { This member function is thread-safe. */ template -requires (std::same_as, AsyncTask> && ...) + requires (std::same_as, AsyncTask> && ...) tf::AsyncTask silent_dependent_async(F&& func, Tasks&&... tasks); /** @@ -789,7 +789,7 @@ requires (std::same_as, AsyncTask> && ...) This member function is thread-safe. */ template - requires (std::same_as, AsyncTask> && ...) + requires (std::same_as, AsyncTask> && ...) tf::AsyncTask silent_dependent_async(P&& params, F&& func, Tasks&&... tasks); /** @@ -824,8 +824,7 @@ requires (std::same_as, AsyncTask> && ...) This member function is thread-safe. */ - template -requires (!std::same_as, AsyncTask>) + template tf::AsyncTask silent_dependent_async(F&& func, I first, I last); /** @@ -862,8 +861,7 @@ requires (!std::same_as, AsyncTask>) This member function is thread-safe. */ - template - requires (!std::same_as, AsyncTask>) + template tf::AsyncTask silent_dependent_async(P&& params, F&& func, I first, I last); // -------------------------------------------------------------------------- @@ -908,7 +906,7 @@ requires (!std::same_as, AsyncTask>) This member function is thread-safe. */ template -requires (std::same_as, AsyncTask> && ...) + requires (std::same_as, AsyncTask> && ...) auto dependent_async(F&& func, Tasks&&... tasks); /** @@ -953,7 +951,7 @@ requires (std::same_as, AsyncTask> && ...) This member function is thread-safe. */ template - requires (std::same_as, AsyncTask> && ...) + requires (std::same_as, AsyncTask> && ...) auto dependent_async(P&& params, F&& func, Tasks&&... tasks); /** @@ -996,8 +994,7 @@ requires (std::same_as, AsyncTask> && ...) This member function is thread-safe. */ - template -requires (!std::same_as, AsyncTask>) + template auto dependent_async(F&& func, I first, I last); /** @@ -1044,8 +1041,7 @@ requires (!std::same_as, AsyncTask>) This member function is thread-safe. */ - template - requires (!std::same_as, AsyncTask>) + template auto dependent_async(P&& params, F&& func, I first, I last); // ---------------------------------------------------------------------------------------------- @@ -1161,16 +1157,16 @@ requires (!std::same_as, AsyncTask>) template void _corun_until(Worker&, P&&); - template + template void _bulk_schedule(Worker&, I, size_t); - template + template void _bulk_schedule(I, size_t); - template + template void _bulk_spill(I, size_t); - template + template void _bulk_spill_round_robin(I, size_t); template @@ -1182,18 +1178,16 @@ requires (!std::same_as, AsyncTask>) template void _silent_async(P&&, F&&, Topology*, NodeBase*); - template - requires (!std::same_as, AsyncTask>) + template auto _dependent_async(P&&, F&&, I, I, Topology*, NodeBase*); - template - requires (!std::same_as, AsyncTask>) + template auto _silent_dependent_async(P&&, F&&, I, I, Topology*, NodeBase*); template void _schedule_async_task(ArgsT&&...); - template + template AsyncTask _schedule_dependent_async_task(I, I, size_t, ArgsT&&...); }; @@ -1593,7 +1587,7 @@ inline void Executor::_spill(Node* item) { // Uses Knuth multiplicative hash on the first pointer to select a buffer, // providing better bit diffusion than the shift-based approach, especially // when the allocator returns pointers with regular low-bit patterns. -template +template void Executor::_bulk_spill(I first, size_t N) { //assert(N != 0); auto b = ((reinterpret_cast(*first) * 2654435761ULL) >> 32) % _buffers.size(); @@ -1606,7 +1600,7 @@ void Executor::_bulk_spill(I first, size_t N) { // order starting from a hash of the first node's pointer. Each buffer's lock // is held only for its chunk, reducing contention compared to sending the // entire batch to a single buffer. -template +template void Executor::_bulk_spill_round_robin(I first, size_t N) { // assert(N != 0); @@ -1643,7 +1637,7 @@ inline void Executor::_schedule(Node* node) { } // Procedure: _schedule -template +template void Executor::_bulk_schedule(Worker& worker, I first, size_t num_nodes) { if(num_nodes == 0) { @@ -1668,7 +1662,7 @@ void Executor::_bulk_schedule(Worker& worker, I first, size_t num_nodes) { } // Procedure: _schedule -template +template inline void Executor::_bulk_schedule(I first, size_t num_nodes) { if(num_nodes == 0) { diff --git a/taskflow/core/graph.hpp b/taskflow/core/graph.hpp index e8c811b60..6e5dd7e45 100644 --- a/taskflow/core/graph.hpp +++ b/taskflow/core/graph.hpp @@ -191,7 +191,7 @@ class TaskParams { class DefaultTaskParams {}; /** -@brief determines if a type is a task parameter type +@brief concept that determines if a type is a task parameter type A type satisfies tf::TaskParams if it is one of the following: + tf::TaskParams @@ -205,7 +205,7 @@ concept TaskParamsLike = StringLike

; /** -@brief determines if a type is a task parameter type (variable template) +@brief concept that determines if a type is a task parameter type (variable template) @tparam P type to check diff --git a/taskflow/core/runtime.hpp b/taskflow/core/runtime.hpp index 6a68002ca..010e0f186 100644 --- a/taskflow/core/runtime.hpp +++ b/taskflow/core/runtime.hpp @@ -346,8 +346,7 @@ class Runtime { executor.run(taskflow).wait(); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template auto dependent_async(F&& func, I first, I last); /** @@ -391,8 +390,7 @@ class Runtime { executor.run(taskflow).wait(); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template auto dependent_async(P&& params, F&& func, I first, I last); // ---------------------------------------------------------------------------------------------- @@ -497,8 +495,7 @@ class Runtime { executor.wait_for_all(); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template tf::AsyncTask silent_dependent_async(F&& func, I first, I last); /** @@ -535,8 +532,7 @@ class Runtime { executor.run(taskflow).wait(); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template tf::AsyncTask silent_dependent_async(P&& params, F&& func, I first, I last); @@ -719,22 +715,20 @@ requires (std::same_as, AsyncTask> && ...) tf::AsyncTask Runtime::silent_dependent_async( P&& params, F&& func, Tasks&&... tasks ){ - std::array array = { std::forward(tasks)... }; + std::array array = { (&tasks)... }; return silent_dependent_async( std::forward

(params), std::forward(func), array.begin(), array.end() ); } // Function: silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template tf::AsyncTask Runtime::silent_dependent_async(F&& func, I first, I last) { return silent_dependent_async(DefaultTaskParams{}, std::forward(func), first, last); } // Function: silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template tf::AsyncTask Runtime::silent_dependent_async( P&& params, F&& func, I first, I last ) { @@ -759,22 +753,20 @@ auto Runtime::dependent_async(F&& func, Tasks&&... tasks) { template requires (std::same_as, AsyncTask> && ...) auto Runtime::dependent_async(P&& params, F&& func, Tasks&&... tasks) { - std::array array = { std::forward(tasks)... }; + std::array array = { (&tasks)... }; return dependent_async( std::forward

(params), std::forward(func), array.begin(), array.end() ); } // Function: dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto Runtime::dependent_async(F&& func, I first, I last) { return dependent_async(DefaultTaskParams{}, std::forward(func), first, last); } // Function: dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto Runtime::dependent_async(P&& params, F&& func, I first, I last) { _node->_join_counter.fetch_add(1, std::memory_order_relaxed); return _executor._dependent_async( diff --git a/taskflow/core/task_group.hpp b/taskflow/core/task_group.hpp index be752b3b9..e04eeb986 100644 --- a/taskflow/core/task_group.hpp +++ b/taskflow/core/task_group.hpp @@ -339,8 +339,7 @@ class TaskGroup { }); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template auto dependent_async(F&& func, I first, I last); /** @@ -385,8 +384,7 @@ class TaskGroup { }); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template auto dependent_async(P&& params, F&& func, I first, I last); @@ -495,8 +493,7 @@ class TaskGroup { }); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template tf::AsyncTask silent_dependent_async(F&& func, I first, I last); /** @@ -534,8 +531,7 @@ class TaskGroup { }); @endcode */ - template - requires (!std::same_as, AsyncTask>) + template tf::AsyncTask silent_dependent_async(P&& params, F&& func, I first, I last); @@ -804,22 +800,20 @@ requires (std::same_as, AsyncTask> && ...) tf::AsyncTask TaskGroup::silent_dependent_async( P&& params, F&& func, Tasks&&... tasks ){ - std::array array = { std::forward(tasks)... }; + std::array array = { (&tasks)... }; return silent_dependent_async( std::forward

(params), std::forward(func), array.begin(), array.end() ); } // Function: silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template tf::AsyncTask TaskGroup::silent_dependent_async(F&& func, I first, I last) { return silent_dependent_async(DefaultTaskParams{}, std::forward(func), first, last); } // Function: silent_dependent_async -template -requires (!std::same_as, AsyncTask>) +template tf::AsyncTask TaskGroup::silent_dependent_async( P&& params, F&& func, I first, I last ) { @@ -844,22 +838,20 @@ auto TaskGroup::dependent_async(F&& func, Tasks&&... tasks) { template requires (std::same_as, AsyncTask> && ...) auto TaskGroup::dependent_async(P&& params, F&& func, Tasks&&... tasks) { - std::array array = { std::forward(tasks)... }; + std::array array = { (&tasks)... }; return dependent_async( std::forward

(params), std::forward(func), array.begin(), array.end() ); } // Function: dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto TaskGroup::dependent_async(F&& func, I first, I last) { return dependent_async(DefaultTaskParams{}, std::forward(func), first, last); } // Function: dependent_async -template -requires (!std::same_as, AsyncTask>) +template auto TaskGroup::dependent_async(P&& params, F&& func, I first, I last) { _node_base._join_counter.fetch_add(1, std::memory_order_relaxed); return _executor._dependent_async(