Static Tasking
This chapter demonstrates how to create a static task dependency graph. Static tasking captures the static parallel structure of a decomposition and is defined only by the program itself. It has a flat task hierarchy and cannot spawn new tasks from a running dependency graph.
Create a Task Dependency Graph
A task in Taskflow is a callable object for which the operation std::operator()
overloaded. All tasks are created from tf::
1: tf::Taskflow taskflow; 2: tf::Task A = taskflow.placeholder(); 3: tf::Task B = taskflow.emplace([] () { std::cout << "task B\n"; }); 4: 5: auto [D, E, F] = taskflow.emplace( 6: [](){ std::cout << "Task A\n"; }, 7: [](){ std::cout << "Task B\n"; }, 8: [](){ std::cout << "Task C\n"; } 9: );
Debrief:
- Line 1 creates a taskflow object, or a graph
- Line 2 creates a placeholder task without work (i.e., callable)
- Line 3 creates a task from a given callable object and returns a task handle
- Lines 5-9 create three tasks in one call using C++ structured binding coupled with std::
tuple
Each time you create a task, the taskflow object creates a node in the task graph and returns a task handle of type tf::
1: tf::Taskflow taskflow; 2: tf::Task A = taskflow.emplace([] () { std::cout << "create a task A\n"; }); 3: tf::Task B = taskflow.emplace([] () { std::cout << "create a task B\n"; }); 4: 5: A.name("TaskA"); 6: A.work([] () { std::cout << "reassign A to a new callable\n"; }); 7: A.precede(B); 8: 9: std::cout << A.name() << std::endl; // TaskA 10: std::cout << A.num_successors() << std::endl; // 1 11: std::cout << A.num_dependents() << std::endl; // 0 12: 13: std::cout << B.num_successors() << std::endl; // 0 14: std::cout << B.num_dependents() << std::endl; // 1
Debrief:
- Line 1 creates a taskflow object
- Lines 2-3 create two tasks A and B
- Lines 5-6 assign a name and a work to task A, and add a precedence link to task B
- Line 7 adds a dependency link from A to B
- Lines 9-14 dump the task attributes
Taskflow uses general-purpose polymorphic function wrapper, std::
taskflow.emplace([ptr=std::make_unique<int>(1)](){ std::cout << "captured unique pointer is not copyable"; });
Visualize a Task Dependency Graph
You can dump a taskflow to a DOT format and visualize the graph using free online tools such as GraphvizOnline and WebGraphviz.
1: #include <taskflow/taskflow.hpp> 2: 3: int main() { 4: 5: tf::Taskflow taskflow; 6: 7: // create a task dependency graph 8: tf::Task A = taskflow.emplace([] () { std::cout << "Task A\n"; }); 9: tf::Task B = taskflow.emplace([] () { std::cout << "Task B\n"; }); 10: tf::Task C = taskflow.emplace([] () { std::cout << "Task C\n"; }); 11: tf::Task D = taskflow.emplace([] () { std::cout << "Task D\n"; }); 12: 13: // add dependency links 14: A.precede(B); 15: A.precede(C); 16: B.precede(D); 17: C.precede(D); 18: 19: taskflow.dump(std::cout); 20: }
Debrief:
- Line 5 creates a taskflow object
- Lines 8-11 create four tasks
- Lines 14-17 add four task dependencies
- Line 19 dumps the taskflow in the DOT format through standard output
Modify Task Attributes
This example demonstrates how to modify a task's attributes using methods defined in the task handler.
1: #include <taskflow/taskflow.hpp> 2: 3: int main() { 4: 5: tf::Taskflow taskflow; 6: 7: std::vector<tf::Task> tasks = { 8: taskflow.placeholder(), // create a task with no work 9: taskflow.placeholder() // create a task with no work 10: }; 11: 12: tasks[0].name("This is Task 0"); 13: tasks[1].name("This is Task 1"); 14: tasks[0].precede(tasks[1]); 15: 16: for(auto task : tasks) { // print out each task's attributes 17: std::cout << task.name() << ": " 18: << "num_dependents=" << task.num_dependents() << ", " 19: << "num_successors=" << task.num_successors() << '\n'; 20: } 21: 22: taskflow.dump(std::cout); // dump the taskflow graph 23: 24: tasks[0].work([](){ std::cout << "got a new work!\n"; }); 25: tasks[1].work([](){ std::cout << "got a new work!\n"; }); 26: 27: return 0; 28: }
The output of this program looks like the following:
This is Task 0: num_dependents=0, num_successors=1 This is Task 1: num_dependents=1, num_successors=0 digraph Taskflow { "This is Task 1"; "This is Task 0"; "This is Task 0" -> "This is Task 1"; }
Debrief:
- Line 5 creates a taskflow object
- Lines 7-10 create two placeholder tasks with no works and stores the corresponding task handles in a vector
- Lines 12-13 name the two tasks with human-readable strings
- Line 14 adds a dependency link from the first task to the second task
- Lines 16-20 print out the name of each task, the number of dependents, and the number of successors
- Line 22 dumps the task dependency graph to a GraphViz Online format (dot)
- Lines 24-25 assign a new target to each task
You can change the name and work of a task at anytime before running the graph. The later assignment overwrites the previous values.
Traverse Adjacent Tasks
You can iterate the successor list and the dependent list of a task by using tf::
// traverse all successors of my_task my_task.for_each_successor([s=0] (tf::Task successor) mutable { std::cout << "successor " << s++ << '\n'; }); // traverse all dependents of my_task my_task.for_each_dependent([d=0] (tf::Task dependent) mutable { std::cout << "dependent " << d++ << '\n'; });
Attach User Data to a Task
You can attach custom data to a task using tf::void*
) you can use to point to user data and access it in the body of a task callable. The following example attaches an integer to a task and accesses that integer through capturing the data in the callable.
int my_data = 5; tf::Task task = taskflow.placeholder(); task.data(&my_data) .work([task](){ int my_date = *static_cast<int*>(task.data()); std::cout << "my_data: " << my_data; });
Notice that you need to create a placeholder task first before assigning it a work callable. Only this way can you capture that task in the lambda and access its attached data in the lambda body.
Understand the Lifetime of a Task
A task lives with its graph and belongs to only a graph at a time, and is not destroyed until the graph gets cleaned up. The lifetime of a task refers to the user-given callable object, including captured values. As long as the graph is alive, all the associated tasks exist.
Move a Taskflow
You can construct or assign a taskflow from a moved taskflow. Moving a taskflow to another will result in transferring the underlying graph data structures from one to the other.
tf::Taskflow taskflow1, taskflow3; taskflow1.emplace([](){}); // move-construct taskflow2 from taskflow1 tf::Taskflow taskflow2(std::move(taskflow1)); assert(taskflow2.num_tasks() == 1 && taskflow1.num_tasks() == 0); // move-assign taskflow3 to taskflow2 taskflow3 = std::move(taskflow2); assert(taskflow3.num_tasks() == 1 && taskflow2.num_tasks() == 0);
You can only move a taskflow to another while that taskflow is not being run by an executor. Moving a running taskflow can result in undefined behavior. Please see Execute a Taskflow with Transferred Ownership for more details.