In this chapter, we demonstrate the basic construct of a task dependency graph - tf::Task.
What is a Task?
A task in Cpp-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.
Cpp-Taskflow provides three methods, tf::Taskflow::placeholder, tf::Taskflow::silent_emplace, and tf::Taskflow::emplace to create a task.
1: auto A = taskflow.placeholder();
2: auto B = taskflow.silent_emplace([] () {});
3: auto [C, FuC] = taskflow.emplace([] () { return 1; });
Debrief:
- Line 1 creates an empty task
- Line 2 creates a task from a given callable object and returns a task handle
- Line 3 creates a task from a given callable object and returns, in addition to a task handle, a std::future object to access the result
Each time you create a task, including an empty one, the taskflow object adds a node to the present graph and returns a task handle of type 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.
4:
5: A.name("TaskA");
6: A.work([] () {
std::cout <<
"reassign A to a new task\n"; });
7: A.precede(B);
8:
12:
Debrief:
- Line 1 creates a taskflow object
- Line 2-3 creates two tasks A and B
- Line 5-6 assigns 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
- Line 9-14 dumps the task attributes
Cpp-Taskflow uses the general-purpose polymorphic function wrapper std::function to store and invoke any callable target in a task. You need to follow its contract to create a task. For instance, the callable object must be copy constructible.
Access the Result of a Task
Unlike tf::Taskflow::silent_emplace, the method tf::Taskflow::emplace returns a pair of a task handle and a std::future object to provide a mechanism to access the result when the associated task finishes. This is particularly useful when you would like to pass data between tasks.
auto [A, FuA] = taskflow.
emplace([](){
return 1; });
You should be aware that every time you add a task or a dependency, it creates only a node or an edge to the present graph. The execution does not start until you dispatch the graph. For example, the following code will block and never finish:
auto [A, FuA] = taskflow.
emplace([](){
return 1; });
Create Multiple Tasks at One Time
Cpp-Taskflow uses C++ structured binding coupled with tuple to make the creation of tasks simple. Both tf::Taskflow::silent_emplace and tf::Taskflow::emplace accept many callable objects to create multiple tasks at one time.
Lifetime of A Task
A task lives with its graph, and is not destroyed until its parent graph gets cleaned up. A task belongs to only a graph at a time. The lifetime of a task mostly refers to the user-given callable object, including captured values. As long as the graph is alive, all the associated tasks remain their existence. We recommend the users to read Lifetime of a Graph.
Example 1: Create Multiple Dependency Graphs
The example below demonstrates how to reuse task handles to create two task dependency graphs.
1: #include <taskflow/taskflow.hpp>
2:
3: int main() {
4:
6:
7:
13: };
14:
15: tasks[0].precede(tasks[1]);
16: tasks[0].precede(tasks[2]);
17: tasks[1].precede(tasks[3]);
18: tasks[2].precede(tasks[3]);
19:
21:
22:
23: tasks = {
28: };
29:
30: tasks[3].precede(tasks[2]);
31: tasks[2].precede(tasks[1]);
32: tasks[1].precede(tasks[0]);
33:
35:
36: return 0;
37: }
Debrief:
- Line 5 creates a taskflow object
- Line 8 creates a task array to store four task handles
- Line 9-12 creates four tasks
- Line 15-18 adds four task dependency links
- Line 20 dispatches the graph and blocks until it completes
- Line 23-28 creates four new tasks and reassigns the task array to these four tasks
- Line 30-32 adds a linear dependency to these four tasks
- Line 34 dispatches the graph and blocks until it completes
Notice that trying to modify a task in a dispatched graph results in undefined behavior. For examples, starting from Line 21, you should not modify any tasks but assign them to new targets (Line 23-28).
Example 2: 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:
6:
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) {
18: << "num_dependents=" << task.num_dependents() << ", "
19: << "num_successors=" << task.num_successors() << '\n';
20: }
21:
23:
24: tasks[0].work([](){
std::cout <<
"got a new work!\n"; });
25: tasks[1].work([](){
std::cout <<
"got a new work!\n"; });
26:
28:
29: return 0;
30: }
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";
}
got a new work!
got a new work!
Debrief:
- Line 5 creates a taskflow object
- Line 7-10 creates two tasks with empty target and stores the corresponding task handles in a vector
- Line 12-13 names the two tasks with human-readable strings
- Line 14 adds a dependency link from the first task to the second task
- Line 16-20 prints 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)
- Line 24-25 assigns a new target to each task
- Line 27 dispatches the graph and blocks until the execution finishes
You can change the name and work of a task at anytime before dispatching the graph. The later assignment overwrites the previous values. Only the latest information will be used.