| 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:1738
tf::FlowBuilder::placeholder
Task placeholder()
creates a placeholder task
Definition flow_builder.hpp:1813
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:1435
tf::Task::num_successors
size_t num_successors() const
queries the number of successors of the task
Definition task.hpp:1455
tf::Task::work
Task & work(C &&callable)
assigns a callable
Definition task.hpp:1538
tf::Task::precede
Task & precede(Ts &&... tasks)
adds precedence links from this to other tasks
Definition task.hpp:1305
tf::Task::num_predecessors
size_t num_predecessors() const
queries the number of predecessors of the task
Definition task.hpp:1440

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

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:1507
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:1499
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:1567

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.