mesytec-mnode/external/taskflow-3.8.0/doxygen/cookbook/static_tasking.dox
2025-01-04 01:25:05 +01:00

266 lines
9 KiB
Text

namespace tf {
/** @page StaticTasking 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.
@tableofcontents
@section CreateATaskDependencyGraph Create a Task Dependency Graph
A task in %Taskflow is a @em 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 @c 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.
@code{.cpp}
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: );
@endcode
Debrief:
@li Line 1 creates a taskflow object, or a @em graph
@li Line 2 creates a placeholder task without work (i.e., callable)
@li Line 3 creates a task from a given callable object and returns a task handle
@li 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.
@code{.cpp}
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
@endcode
Debrief:
@li Line 1 creates a taskflow object
@li Lines 2-3 create two tasks A and B
@li Lines 5-6 assign a name and a work to task A, and add a precedence link to task B
@li Line 7 adds a dependency link from A to B
@li 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:
@code{.cpp}
taskflow.emplace([ptr=std::make_unique<int>(1)](){
std::cout << "captured unique pointer is not copyable";
});
@endcode
@section VisualizeATaskDependencyGraph Visualize a Task Dependency Graph
You can dump a taskflow to a DOT format and visualize the graph using free online tools such as <a href="https://dreampuf.github.io/GraphvizOnline/">GraphvizOnline</a> and <a href="http://www.webgraphviz.com/">WebGraphviz</a>.
@code{.cpp}
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: }
@endcode
Debrief:
@li Line 5 creates a taskflow object
@li Lines 8-11 create four tasks
@li Lines 14-17 add four task dependencies
@li Line 19 dumps the taskflow in the DOT format through standard output
<!-- @image html images/simple.svg width=40% -->
@dotfile images/simple.dot
@section ModifyTaskAttributes Modify Task Attributes
This example demonstrates how to modify a task's attributes using methods defined in
the task handler.
@code{.cpp}
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: }
@endcode
The output of this program looks like the following:
@code{.sh}
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";
}
@endcode
Debrief:
@li Line 5 creates a taskflow object
@li Lines 7-10 create two placeholder tasks with no works and stores the corresponding task handles in a vector
@li Lines 12-13 name the two tasks with human-readable strings
@li Line 14 adds a dependency link from the first task to the second task
@li Lines 16-20 print out the name of each task, the number of dependents, and the number of successors
@li Line 22 dumps the task dependency graph to a @GraphVizOnline format (dot)
@li 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.
@section TraverseAdjacentTasks 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.
@code{.cpp}
// 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';
});
@endcode
@section AttachUserDataToATask 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.
@code{.cpp}
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;
});
@endcode
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.
@section UnderstandTheLifetimeOfATask 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.
@section MoveATaskflow Move a Taskflow
You can construct or assign a taskflow from a @em moved taskflow.
Moving a taskflow to another will result in transferring the underlying graph
data structures from one to the other.
@code{.cpp}
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);
@endcode
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 @ref ExecuteATaskflowWithTransferredOwnership for more details.
*/
}