StaticTasking

# Static Tasking

Create a Task Dependency Graph

Create a Task Dependency Graph

StaticTasking_1CreateATaskDependencyGraph
Visualize a Task Dependency Graph

Visualize a Task Dependency Graph

StaticTasking_1VisualizeATaskDependencyGraph
Modify Task Attributes

Modify Task Attributes

StaticTasking_1ModifyTaskAttributes
Traverse Adjacent Tasks

Traverse Adjacent Tasks

StaticTasking_1TraverseAdjacentTasks
Attach User Data to a Task

Attach User Data to a Task

StaticTasking_1AttachUserDataToATask
Understand the Lifetime of a Task

Understand the Lifetime of a Task

StaticTasking_1UnderstandTheLifetimeOfATask
Move a Taskflow

Move a Taskflow

StaticTasking_1MoveATaskflow

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::Taskflowtaskflow;
    2:tf::TaskA=taskflow.placeholder();
    3:tf::TaskB=taskflow.emplace([](){std::cout<<"taskB\n";});
    4:
    5:auto[D,E,F]=taskflow.emplace(
    6:[](){std::cout<<"TaskA\n";},
    7:[](){std::cout<<"TaskB\n";},
    8:[](){std::cout<<"TaskC\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::Taskflowtaskflow;
    2:tf::TaskA=taskflow.emplace([](){std::cout<<"createataskA\n";});
    3:tf::TaskB=taskflow.emplace([](){std::cout<<"createataskB\n";});
    4:
    5:A.name("TaskA");
    6:A.work([](){std::cout<<"reassignAtoanewcallable\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<<"captureduniquepointerisnotcopyable";
    });

# 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:intmain(){
    4:
    5:tf::Taskflowtaskflow;
    6:
    7://createataskdependencygraph
    8:tf::TaskA=taskflow.emplace([](){std::cout<<"TaskA\n";});
    9:tf::TaskB=taskflow.emplace([](){std::cout<<"TaskB\n";});
    10:tf::TaskC=taskflow.emplace([](){std::cout<<"TaskC\n";});
    11:tf::TaskD=taskflow.emplace([](){std::cout<<"TaskD\n";});
    12:
    13://adddependencylinks
    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:intmain(){
    4:
    5:tf::Taskflowtaskflow;
    6:
    7:std::vector<tf::Task>tasks={
    8:taskflow.placeholder(),//createataskwithnowork
    9:taskflow.placeholder()//createataskwithnowork
    10:};
    11:
    12:tasks[0].name("ThisisTask0");
    13:tasks[1].name("ThisisTask1");
    14:tasks[0].precede(tasks[1]);
    15:
    16:for(autotask:tasks){//printouteachtask'sattributes
    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);//dumpthetaskflowgraph
    23:
    24:tasks[0].work([](){std::cout<<"gotanewwork!\n";});
    25:tasks[1].work([](){std::cout<<"gotanewwork!\n";});
    26:
    27:return0;
    28:}

The output of this program looks like the following:

    ThisisTask0:num_dependents=0,num_successors=1
    ThisisTask1:num_dependents=1,num_successors=0
    digraphTaskflow{
    "ThisisTask1";
    "ThisisTask0";
    "ThisisTask0"->"ThisisTask1";
    }

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.

    //traverseallsuccessorsofmy_task
    my_task.for_each_successor([s=0](tf::Tasksuccessor)mutable{
    std::cout<<"successor"<<s++<<'\n';
    });

    //traversealldependentsofmy_task
    my_task.for_each_dependent([d=0](tf::Taskdependent)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.

    intmy_data=5;
    tf::Tasktask=taskflow.placeholder();
    task.data(&my_data)
    .work([task](){
    intmy_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.

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

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::Taskflowtaskflow1,taskflow3;

    taskflow1.emplace([](){});

    //move-constructtaskflow2fromtaskflow1
    tf::Taskflowtaskflow2(std::move(taskflow1));
    assert(taskflow2.num_tasks()==1&&taskflow1.num_tasks()==0);

    //move-assigntaskflow3totaskflow2
    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.