Cpp-Taskflow  2.1.0
C1: Understand the Task

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 two methods, tf::Taskflow::placeholder and tf::Taskflow::emplace to create a task.

tf::Taskflow taskflow;
tf::Task A = taskflow.placeholder();
tf::Task B = taskflow.emplace([] () { std::cout << "task B\n"; });

Debrief:

  • Line 2 creates an empty task
  • Line 3 creates a task from a given callable object and returns a task handle

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.

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 task\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
  • 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.

Create Multiple Tasks at One Time

Cpp-Taskflow uses C++ structured binding coupled with tuple to make the creation of tasks simple. The mthod tf::Taskflow::emplace accepts a arbitrary number of callable objects to create multiple tasks at one time.

auto [A, B, C] = taskflow.emplace( // create three tasks in one call
[](){ std::cout << "Task A\n"; },
[](){ std::cout << "Task B\n"; },
[](){ std::cout << "Task C\n"; }
);

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:
5: tf::Taskflow taskflow;
6:
7: // create a task dependency graph
8:
9: tf::Task t0 = taskflow.emplace([] () { std::cout << "Task A\n"; });
10: tf::Task t1 = taskflow.emplace([] () { std::cout << "Task B\n"; });
11: tf::Task t2 = taskflow.emplace([] () { std::cout << "Task C\n"; });
12: tf::Task t3 = taskflow.emplace([] () { std::cout << "Task D\n"; });
13:
14: // add dependency links
15: t0.precede(t1);
16: t0.precede(t2);
17: t1.precede(t3);
18: t2.precede(t3);
19:
20: taskflow.wait_for_all();
21:
22: // create another task dependency graph
23: // we can reuse the task handle
24: t0 = taskflow.emplace([] () { std::cout << "New Task A\n"; });
25: t1 = taskflow.emplace([] () { std::cout << "New Task B\n"; });
26: t2 = taskflow.emplace([] () { std::cout << "New Task C\n"; });
27: t3 = taskflow.emplace([] () { std::cout << "New Task D\n"; });
28:
29: // add dependency links
30: t3.precede(t2);
31: t2.precede(t1);
32: t1.precede(t0);
33:
34: taskflow.wait_for_all();
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:
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: taskflow.wait_for_all();
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.