Cpp-Taskflow
2.0.0
|
After you create a task dependency graph, you need to dispatch it to threads for execution. In this chapter, we will show you how to execute a task dependency graph.
Each taskflow object has exactly one graph at a time that represents the tasks and the dependencies constructed so far. The graph exists until users dispatch it for execution. In Cpp-Taskflow, we call a dispatched graph a topology. A topology is a data structure that wraps up a dispatched graph and stores a few metadata obtained at runtime. Each taskflow object has a list of topologies to keep track of the execution status of dispatched graphs. Users can retrieve this information later on for graph inspection and debugging.
All tasks are executed in a shared thread storage coupled with a task scheduler to decide which thread runs which task. Cpp-Taskflow provides two ways to dispatch a task dependency graph, blocking and non-blocking executions.
One way to dispatch the present task dependency graph is to use the method tf::Taskflow::wait_for_all. Calling wait_for_all
dispatches the graph to threads and blocks the program flow until all tasks finish.
When wait_for_all
returns, all tasks including previously dispatched ones are guaranteed to finish. All topologies will be cleaned up as well.
Another way to dispatch the present task dependency graph is to use the method tf::Taskflow::dispatch or tf::Taskflow::silent_dispatch. These two methods both dispatch the present graph to threads and return immediately without blocking the program flow. Non-blocking methods allow the program to perform other computations that can overlap the graph execution.
Debrief:
The method dispatch
has a overload that takes a callable object to execute when the dispatched graph finishes.
If you do not care the status of a dispatched graph, use the method tf::Taskflow::silent_dispatch. This method does not return anything.
Similarly, the method silent_dispatch
has an overload that takes a callable object to execute when the dispatched graph finishes.
Unlike tf::Taskflow::wait_for_all, calling tf::Taskflow::dispatch or tf::Taskflow::silent_dispatch will not clean up the topologies upon completion. This allows users to dump the graph structure, in particular, created from dynamic tasking to debug the dependency graph. However, it may be necessary at some points of the program to synchronize with the previously dispatched graphs. Cpp-Taskflow provides a method tf::Taskflow::wait_for_topologies for this purpose.
Debrief
It is clear now Line 9 overlaps the execution of the first graph. After Line 11, there are two topologies in the taskflow object. Calling the method tf::Taskflow::wait_for_topologies blocks the program until both graph complete.
In Cpp-Taskflow, the lifetime of a task sticks with its parent graph. The lifetime of a task mostly refers to the user-given callable objects, including those captured by a lambda expression. When a graph is destroyed, all of its tasks are destroyed. Consider the following example that uses std::shared_ptr to demonstrate the lifetime of a graph and the impact on its task.
The output is as follows:
In Line 5, we created a shared pointer object with one reference count on itself. After Line 7-13, the reference count increases by two because task A and task B both capture a copy of the shared pointer. The lifetime of a task has nothing to do with its dependency constraints, as shown in Line 8, Line 12, and Line 17. However, Line 19 dispatches the graph to threads and cleans up its data structure upon finish, including all associated tasks. Therefore, the reference count in Line 21 drops down to one (owner at Line 3).
Now let's use the same example but dispatch the graph asynchronously.
In Line 19, we replace wait_for_all
with dispatch
to dispatch the graph asynchronously without cleaning up its data structures upon completion. In other words, task A and task B remains un-destructed after Line 19, and the reference count at this point remains three.
The example below demonstrates how to create multiple task dependency graphs and dispatch each of them asynchronously.
Debrief:
Notice in Line 24 the counter ends up being 20. By default, destructing a taskflow object will wait on all topologies to finish.
The example demonstrates how to use the std::future to explicitly impose a dependency link on two dispatched graphs.
Debrief:
sum
to zero, and overlaps its execution with the first dependency graph By the time the second dependency graph finishes, the first dependency graph must have already finished due to Line 31. The result of the variable sum
ends up being the summation over the integer sequence [0, 1, 2, ..., 1024).