Cookbook » Static Tasking | Taskflow QuickStart

# Cookbook » 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::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:

- 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::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_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::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";
    });

## 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::Task::for_each_successor and tf::Task::for_each_dependent, respectively. Each method takes a lambda and applies it to a successor or a dependent 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 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::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.

#### Attention

It is your responsibility to ensure that the attached data stay alive during the execution of its task.

## 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.

#### Attention

It is your responsibility to keep tasks and graph alive during their execution.

## 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.

Tab / T to search, Esc to close
…

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.

Sorry, nothing was found.