Subflow Tasking
It is very common for a parallel program to spawn task dependency graphs at runtime. In Taskflow, we call this subflow tasking.
Create a Subflow
Subflow tasks are those created during the execution of a graph. These tasks are spawned from a parent task and are grouped together to a subflow dependency graph. To create a subflow, emplace a callable that takes an argument of type tf::
1: tf::Taskflow taskflow; 2: tf::Executor executor; 3: 4: tf::Task A = taskflow.emplace([] () {}).name("A"); // static task A 5: tf::Task C = taskflow.emplace([] () {}).name("C"); // static task C 6: tf::Task D = taskflow.emplace([] () {}).name("D"); // static task D 7: 8: tf::Task B = taskflow.emplace([] (tf::Subflow& subflow) { 9: tf::Task B1 = subflow.emplace([] () {}).name("B1"); // subflow task B1 10: tf::Task B2 = subflow.emplace([] () {}).name("B2"); // subflow task B2 11: tf::Task B3 = subflow.emplace([] () {}).name("B3"); // subflow task B3 12: B1.precede(B3); // B1 runs before B3 13: B2.precede(B3); // B2 runs before B3 14: }).name("B"); 15: 16: A.precede(B); // B runs after A 17: A.precede(C); // C runs after A 18: B.precede(D); // D runs after B 19: C.precede(D); // D runs after C 20: 21: executor.run(taskflow).get(); // execute the graph to spawn the subflow 22: taskflow.dump(std::cout); // dump the taskflow to a DOT format
Debrief:
- Lines 1-2 create a taskflow and an executor
- Lines 4-6 create three tasks, A, C, and D
- Lines 8-14 create a task B that spawns a task dependency graph of three tasks B1, B2, and B3
- Lines 16-19 add dependencies among A, B, C, and D
- Line 21 submits the graph to an executor and waits until it finishes
- Line 22 dumps the entire task dependency graph
Lines 8-14 are the main block to enable subflow tasking at task B. The runtime will create a tf::
Join a Subflow
By default, a subflow joins its parent task when the program leaves its execution context. All nodes of zero outgoing edges in the subflow precede its parent task. You can explicitly join a subflow within its execution context to carry out recursive patterns. A famous implementation is fibonacci recursion.
int spawn(int n, tf::Subflow& sbf) { if (n < 2) return n; int res1, res2; sbf.emplace([&res1, n] (tf::Subflow& sbf) { res1 = spawn(n - 1, sbf); } ); sbf.emplace([&res2, n] (tf::Subflow& sbf) { res2 = spawn(n - 2, sbf); } ); sbf.join(); // join to materialize the subflow immediately return res1 + res2; } taskflow.emplace([&res] (tf::Subflow& sbf) { res = spawn(5, sbf); }); executor.run(taskflow).wait();
The code above computes the fifth fibonacci number using recursive subflow. Calling tf::
Our implementation to join subflows is recursive in order to preserve the thread context in each subflow task. Having a deep recursion of subflows may cause stack overflow.
Detach a Subflow
In contract to joined subflow, you can detach a subflow from its parent task, allowing its execution to flow independently.
1: tf::Taskflow taskflow; 2: 3: tf::Task A = taskflow.emplace([] () {}).name("A"); // static task A 4: tf::Task C = taskflow.emplace([] () {}).name("C"); // static task C 5: tf::Task D = taskflow.emplace([] () {}).name("D"); // static task D 6: 7: tf::Task B = taskflow.emplace([] (tf::Subflow& subflow) { 8: tf::Task B1 = subflow.emplace([] () {}).name("B1"); // static task B1 9: tf::Task B2 = subflow.emplace([] () {}).name("B2"); // static task B2 10: tf::Task B3 = subflow.emplace([] () {}).name("B3"); // static task B3 11: B1.precede(B3); // B1 runs before B3 12: B2.precede(B3); // B2 runs before B3 13: subflow.detach(); // detach this subflow 14: }).name("B"); 15: 16: A.precede(B); // B runs after A 17: A.precede(C); // C runs after A 18: B.precede(D); // D runs after B 19: C.precede(D); // D runs after C 20: 21: tf::Executor executor; 22: executor.run(taskflow).wait(); // execute the graph to spawn the subflow 22: taskflow.dump(std::cout); // dump the taskflow to DOT format
The figure below demonstrates a detached subflow based on the previous example. A detached subflow will eventually join the topology of its parent task.
Detached subflow becomes an independent graph attached to the top-most taskflow. Running a taskflow multiple times will accumulate all detached tasks in the graph. For example, running the above taskflow 5 times results in a total of 19 tasks.
executor.run_n(taskflow, 5).wait(); assert(taskflow.num_tasks() == 19); taskflow.dump(std::cout);
The dumped graph is shown as follows:
Create a Nested Subflow
A subflow can be nested or recursive. You can create another subflow from the execution of a subflow and so on.
1: tf::Taskflow taskflow; 2: 3: tf::Task A = taskflow.emplace([] (tf::Subflow& sbf){ 4: std::cout << "A spawns A1 & subflow A2\n"; 5: tf::Task A1 = sbf.emplace([] () { 6: std::cout << "subtask A1\n"; 7: }).name("A1"); 8: 9: tf::Task A2 = sbf.emplace([] (tf::Subflow& sbf2){ 10: std::cout << "A2 spawns A2_1 & A2_2\n"; 11: tf::Task A2_1 = sbf2.emplace([] () { 12: std::cout << "subtask A2_1\n"; 13: }).name("A2_1"); 14: tf::Task A2_2 = sbf2.emplace([] () { 15: std::cout << "subtask A2_2\n"; 16: }).name("A2_2"); 17: A2_1.precede(A2_2); 18: }).name("A2"); 19: A1.precede(A2); 20: }).name("A"); 21: 22: // execute the graph to spawn the subflow 23: tf::Executor().run(taskflow).get(); 24: taskflow.dump(std::cout);
Debrief:
- Line 1 creates a taskflow object
- Lines 3-20 create a task to spawn a subflow of two tasks A1 and A2
- Lines 9-18 spawn another subflow of two tasks A2_1 and A2_2 out of its parent task A2
- Lines 23-24 runs the graph asynchronously and dump its structure when it finishes
Similarly, you can detach a nested subflow from its parent subflow. A detached subflow will run independently and eventually join the topology of its parent subflow.