Back to Taskflow

Create a Task Dependency Graph

docs/StaticTasking.html

4.1.015.6 KB
Original Source

| | Taskflow: A General-purpose Task-parallel Programming System |

Loading...

Searching...

No Matches

Static Tasking

Static tasking is the most basic programming model in Taskflow. It follows the construct-and-run model, where you define a taskflow graph first and submit it to an executor for execution.

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.

For example, the code below creates a taskflow. It first defines a placeholder task without assigned work, then creates a task directly from a given callable and obtains its task handle. Finally, it creates multiple tasks in one call using C++ structured binding.

tf::Taskflow taskflow;

tf::Task A = taskflow.placeholder();

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

auto [D, E, F] = taskflow.emplace(

{ std::cout << "Task D\n"; },

{ std::cout << "Task E\n"; },

{ std::cout << "Task F\n"; }

);

tf::FlowBuilder::emplace

Task emplace(C &&callable)

creates a static task

Definition flow_builder.hpp:1571

tf::FlowBuilder::placeholder

Task placeholder()

creates a placeholder task

Definition flow_builder.hpp:1635

tf::Task

class to create a task handle over a taskflow node

Definition task.hpp:569

tf::Taskflow

class to create a taskflow object

Definition taskflow.hpp:64

Each time you create a task, the taskflow creates a node and returns a task handle of type tf::Task. A task handle is a copy-cheap wrapper over the node pointer to a task in taskflow. The handle provides a set of methods for you to access and modify the attributes of a task, such as building dependencies, assigning a name, changing the work, querying task statistics, and so on.

tf::Taskflow taskflow;

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

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

A.name("Task A");

A.work([] () { std::cout << "reassign A to a new callable\n"; });

A.precede(B);

std::cout << A.name() << '\n'; // Task A

std::cout << A.num_successors() << '\n'; // 1

std::cout << A.num_predecessors() << '\n'; // 0

std::cout << B.name() << '\n'; // (empty name)

std::cout << B.num_successors() << '\n'; // 0

std::cout << B.num_predecessors() << '\n'; // 1

tf::Task::name

const std::string & name() const

queries the name of the task

Definition task.hpp:1388

tf::Task::num_successors

size_t num_successors() const

queries the number of successors of the task

Definition task.hpp:1408

tf::Task::work

Task & work(C &&callable)

assigns a callable

Definition task.hpp:1491

tf::Task::precede

Task & precede(Ts &&... tasks)

adds precedence links from this to other tasks

Definition task.hpp:1258

tf::Task::num_predecessors

size_t num_predecessors() const

queries the number of predecessors of the task

Definition task.hpp:1393

The code above creates a taskflow of two tasks A and B. It then assigns a name and a new callable to task A, and establishes a precedence link to task B. Finally, it queries the task attributes, including names, successor counts, and predecessor counts of A and B.

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. For example, the code below dumps the taskflow through the standard output std::cout.

#include <taskflow/taskflow.hpp>

int main() {

tf::Taskflow taskflow;

// create a task dependency graph

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

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

tf::Task C = taskflow.emplace([] () { std::cout << "Task C\n"; });

tf::Task D = taskflow.emplace([] () { std::cout << "Task D\n"; });

// add dependency links

A.precede(B);

A.precede(C);

B.precede(D);

C.precede(D);

taskflow.dump(std::cout);

}

tf::Taskflow::dump

void dump(std::ostream &ostream) const

dumps the taskflow to a DOT format through a std::ostream target

Definition taskflow.hpp:433

Embedded content

Visualization helps you understand how tasks and dependencies are structured, making it easier to analyze and debug your taskflow programs.

Traverse Adjacent Tasks

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. Both methods take a unary function that takes an argument of type tf::Task pointing to the task that is being visited.

// 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';

});

Together with tf::Taskflow::for_each_task, you can traverse a taskflow graph. For example, the code below traverse a taskflow and outputs the successor and the predecessor information of each task:

// traverse every task in taskflow using the given unary function

taskflow.for_each_task([](tf::Task task){

// print the name of the task

std::cout << “Task ” << task.name() << ‘\n’;

// traverse all successors of the task

task.for_each_successor([](tf::Task s) mutable {

std::cout << task.name() << “->” << s.name() << ‘ ’;

});

std::cout << “\n”;

// traverse all predecessors of the task

task.for_each_predecessor([] (tf::Task p) mutable {

std::cout << p.name() << “->” << task.name() << ‘ ’;

});

});

tf::Task::for_each_predecessor

void for_each_predecessor(V &&visitor) const

applies an visitor callable to each predecessor of the task

Definition task.hpp:1460

tf::Task::for_each_successor

void for_each_successor(V &&visitor) const

applies an visitor callable to each successor of the task

Definition task.hpp:1452

tf::Taskflow::for_each_task

void for_each_task(V &&visitor) const

applies a visitor to each task in this taskflow

Definition taskflow.hpp:402

If the task contains a subflow (see Subflow Tasking), you can use tf::Task::for_each_subflow_task to iterate all tasks associated with that subflow.

my_task.for_each_subflow_task([](tf::Task stask){

std::cout << "subflow task " << stask.name() << '\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;

});

tf::Task::data

Task & data(void *data)

assigns pointer to user data

Definition task.hpp:1520

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. Also, as Taskflow does not manage any user data, it is your responsibility to ensure any attached data stays alive during the??

Understand the Lifetime of a Task

A task belongs to a single graph at a time and remains alive as long as that graph exists. The lifetime of a task is particularly important when referring to its callable, including any captured values. When the graph is destroyed or cleaned up, all associated tasks are also destroyed. Consequently, it is your responsibility to keep relevant taskflows alive during their execution. For example, the code below can crash because the taskflow may be destroyed before the executor finishes running it, leaving the executor with dangling references to the task graph.

tf::Executor executor;

{

tf::Taskflow taskflow;

taskflow.emplace(

{ std::cout << "Task A\n"; },

{ std::cout << "Task B\n"; },

{ std::cout << "Task C\n"; },

{ std::cout << "Task D\n"; }

);

executor.run(taskflow);

} // taskflow is destroyed after the compound statement

executor.wait_for_all();

tf::Executor

class to create an executor

Definition executor.hpp:62

tf::Executor::run

tf::Future< void > run(Taskflow &taskflow)

runs a taskflow once

tf::Executor::wait_for_all

void wait_for_all()

waits for all tasks to complete

Move a Taskflow

You can construct or assign a taskflow using C++ move semantics. 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({});

// constructs taskflow2 from taskflow1 using C++ move semantics

tf::Taskflow taskflow2(std::move(taskflow1));

assert(taskflow2.num_tasks() == 1 && taskflow1.num_tasks() == 0);

// assigns taskflow2 to taskflow3 using C++ move semantics

taskflow3 = std::move(taskflow2);

assert(taskflow3.num_tasks() == 1 && taskflow2.num_tasks() == 0);

tf::Taskflow::num_tasks

size_t num_tasks() const

queries the number of tasks in this taskflow

Definition taskflow.hpp:376

You can only move a taskflow to another taskflow when it is not being used, such as being executed by an executor. Moving a taskflow that is being used may result in undefined behavior.