| [ Web Proxy ] |
| Viewing: https://raw.githubusercontent.com/ModuleWorks/taskflow/task_isolation/docs/StaticTasking.html | [Back] [Original] |
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.
A task in Taskflow is a callable object for which the operation std::invoke is applicable. It can be either a functor, a lambda expression, a bind expression, or a class objects with operator() overloaded. All tasks are created from tf::Taskflow, the class that manages a task dependency graph. Taskflow provides two methods, tf::Taskflow::placeholder and tf::Taskflow::emplace to create a task.
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:
Each time you create a task, the taskflow object creates a node in the task graph and returns a task handle of type tf::Task. A task handle is a lightweight object that wraps up a particular node in a graph and provides a set of methods for you to assign different attributes to the task such as adding dependencies, naming, and assigning a new work.
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_predecessors() << std::endl; // 0 12: 13: std::cout << B.num_successors() << std::endl; // 0 14: std::cout << B.num_predecessors() << std::endl; // 1
Debrief:
Taskflow uses general-purpose polymorphic function wrapper, std::function, to store and invoke a callable in a task. You need to follow its contract to create a task. For example, the callable to construct a task must be copyable, and thus the code below won't compile:
taskflow.emplace([ptr=std::make_unique<int>(1)](){ std::cout << "captured unique pointer is not copyable"; });
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:
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_predecessors=" << task.num_predecessors() << ", " 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_predecessors=0, num_successors=1 This is Task 1: num_predecessors=1, num_successors=0 digraph Taskflow { "This is Task 1"; "This is Task 0"; "This is Task 0" -> "This is Task 1"; }
Debrief:
You can change the name and work of a task at anytime before running the graph. The later assignment overwrites the previous values.
You can iterate the successor list and the predecessor list of a task by using tf::Task::for_each_successor and tf::Task::for_each_predecessor, respectively. Each method takes a lambda and applies it to a successor or a predecessor being traversed.
// traverse all successors of my_task my_task.for_each_successor([s=0] (tf::Task successor) mutable { std::cout << "successor " << s++ << '\n'; }); // traverse all predecessors of my_task my_task.for_each_predecessor([d=0] (tf::Task predecessor) mutable { std::cout << "predecessor " << d++ << '\n'; });
You can attach custom data to a task using tf::Task::data(void*) and access it using tf::Task::data(). Each node in a taskflow is associated with a C-styled data pointer (i.e., 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.
It is your responsibility to ensure that the attached data stay alive during the execution of its 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.
It is your responsibility to keep tasks and graph alive during their execution.
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.
Search for symbols, directories, files, pages or
modules. You can omit any prefix from the symbol or file path; adding a
: or / suffix lists all members of given symbol or
directory.
Use ↓ / ↑ to navigate through the list, Enter to go. Tab autocompletes common prefix, you can copy a link to the result using L while M produces a Markdown link.
Taskflow handbook is part of the Taskflow project, copyright Dr. Tsung-Wei Huang, 2018–2025.
Generated by Doxygen 1.12.0 and m.css.
| Web Proxy Viewer | New URL | Original Page |