We study a workload whose task graph topology is determined entirely at runtime, where the number of tasks, their work, and which task depends on which are all read from external data rather than hard-coded. This example showcases dependent-async tasks as the right tool when static task graph construction is not possible.
Consider a computational workflow described by an external source such as a configuration file, a database, or the output of a previous computation. Each workflow step has a unique identifier, a list of steps that must complete before it starts, and a function to execute.
For concreteness we represent this as a vector of Step structures:
The steps might be loaded at startup from a JSON file, a database query, or any external source. The key point is that neither their count nor their dependency graph is known when the program is compiled.
The standard Taskflow pattern builds the entire graph with tf::Taskflow::emplace, wires all edges, then calls tf::Executor::run. This requires the full graph to be known before execution begins. When the graph depends on external data, all the data must be loaded first, then the entire taskflow is built, then execution starts. This means all the data must be loaded before any computation begins.
For large workflows this is wasteful: if step 0 has no dependencies, it could start executing the moment it is read, while the remaining steps are still being loaded. Dependent-async tasks solve this by scheduling each task the moment it is created, overlapping graph construction with task execution.
We process the workflow one step at a time. For each step we create a dependent-async task whose predecessors are the tf::AsyncTask handles of previously created steps. The executor begins running each task the moment its predecessors finish, overlapping graph construction with task execution:
The key loop processes one step at a time. By the time we create the task for step 3, step 0 may already be running (or even done), because its task was submitted with no predecessors and began executing immediately. The graph is built and executed simultaneously rather than sequentially.
The topology itself can branch based on properties evaluated during execution. In the example below, the graph structure after step 0 depends on a condition that is only known after step 0 completes:
This pattern is impossible to express with a static tf::Taskflow, where there is no way to know which branch to build before compute_initial_result has run. Dependent-async tasks make the runtime decision a natural part of the program's control flow.
fu0.get() to keep the calling thread active in the work-stealing loop. If the calling thread is itself one of the executor's workers, calling fu0.get() would block the thread, reducing the available parallelism for the tasks it could otherwise be executing. See Asynchronous Tasking with Dependencies for a full discussion.