Loading...
Searching...
No Matches
Runtime Tasking

Taskflow allows a task to interact directly with the scheduling runtime by taking a tf::Runtime object as its argument. Through this handle, a task can spawn new work dynamically, synchronise with sub-tasks cooperatively, and implement recursive parallel algorithms — capabilities that are not possible with ordinary static tasks. We recommend reading Asynchronous Tasking and Asynchronous Tasking with Dependencies before this page.

What is a Runtime Task?

An ordinary Taskflow task is a pure callable with no connection to the scheduler that runs it. A runtime task breaks this boundary by accepting a tf::Runtime& parameter, giving the task a live handle to the executor. Any callable that takes tf::Runtime& as its argument is automatically recognized as a runtime task by Taskflow:

tf::Executor executor;
tf::Taskflow taskflow;
taskflow.emplace([&](tf::Runtime& rt) {
assert(&rt.executor() == &executor); // rt provides access to the executor
});
executor.run(taskflow).wait();
class to create an executor
Definition executor.hpp:62
tf::Future< void > run(Taskflow &taskflow)
runs a taskflow once
Task emplace(C &&callable)
creates a static task
Definition flow_builder.hpp:1558
class to create a runtime task
Definition runtime.hpp:47
Executor & executor()
obtains the running executor
Definition runtime.hpp:621
class to create a taskflow object
Definition taskflow.hpp:64

The real power of a runtime task lies in its ability to spawn and synchronise sub-tasks on the fly, described in the sections below.

Spawn Tasks from a Runtime

Async Tasks

tf::Runtime::async and tf::Runtime::silent_async launch unordered async tasks from within a running runtime task. These calls are thread-safe and place the new tasks immediately into the executor's work-stealing pool.

A key property of runtime-spawned tasks is implicit synchronisation: all tasks spawned from a tf::Runtime are guaranteed to finish before the runtime task itself completes and control passes to the next task in the graph. You do not need to manually join them — the runtime handles this automatically at the end of its scope.

The example below spawns 1000 async tasks from runtime task A. Task B runs after A in the static graph. Thanks to the implicit join, B is guaranteed to observe counter == 1000 with no additional synchronisation required:

tf::Executor executor;
tf::Taskflow taskflow;
std::atomic<size_t> counter{0};
tf::Task A = taskflow.emplace([&](tf::Runtime& rt) {
for(size_t i = 0; i < 1000; i++) {
rt.silent_async([&]() {
counter.fetch_add(1, std::memory_order_relaxed);
});
}
// implicit join: all 1000 tasks finish before A completes
});
tf::Task B = taskflow.emplace([&]() {
assert(counter == 1000); // always holds
});
A.precede(B);
executor.run(taskflow).wait();
void silent_async(F &&f)
runs the given function asynchronously without returning any future object
Definition runtime.hpp:671
class to create a task handle over a taskflow node
Definition task.hpp:263
Task & precede(Ts &&... tasks)
adds precedence links from this to other tasks
Definition task.hpp:952

Dependent Async Tasks

tf::Runtime::dependent_async and tf::Runtime::silent_dependent_async let you build a dynamic task graph with explicit dependency edges from within a runtime task. This mirrors the executor-level API in Asynchronous Tasking with Dependencies, with the same implicit synchronisation guarantee: all dependent-async tasks spawned from the runtime are joined before the runtime task completes.

The example below builds a sequential chain of 1001 dependent-async tasks inside a single runtime task. Each task asserts a specific value of counter, which is enforced by the dependency edges:

tf::Executor executor;
tf::Taskflow taskflow;
taskflow.emplace([](tf::Runtime& rt) {
int counter = 0;
// first task in the chain
assert(counter == 0);
++counter;
});
// extend the chain: each task depends on the one before it
for(size_t i = 1; i <= 1000; i++) {
assert(counter == static_cast<int>(i));
++counter;
}, prev);
prev = curr;
}
// implicit join: the entire chain completes before the runtime task ends
});
executor.run(taskflow).wait();
class to hold a dependent asynchronous task with shared ownership
Definition async_task.hpp:45
tf::AsyncTask silent_dependent_async(F &&func, Tasks &&... tasks)
runs the given function asynchronously when the given predecessors finish
Definition runtime.hpp:710

Issue Cooperative Execution

tf::Runtime::corun allows a runtime task to explicitly wait for all its currently spawned sub-tasks to finish at any point during execution, not just at the end of its scope. Unlike a blocking wait, corun does not suspend the calling worker. Instead, the worker remains active in the executor's work-stealing loop, picking up and executing available tasks while waiting — keeping all threads productive.

A particularly important advantage of this cooperative model is that corun preserves the call stack of the invoking runtime task. The runtime task stays live on the worker while corun executes; when all sub-tasks finish, execution resumes exactly where it left off with all local variables and state intact. This makes it possible to implement recursive parallel algorithms where each level of recursion spawns sub-tasks and then waits cooperatively, building up a tree of live runtime contexts without ever blocking a thread.

The example below implements parallel Fibonacci using recursive runtime tasks. At each level, the left child is spawned as an async runtime task while the right child is computed inline. rt.corun() then waits cooperatively for the left child, resuming with the local variables res1 and res2 exactly as they were:

#include <taskflow/taskflow.hpp>
size_t fibonacci(size_t N, tf::Runtime& rt) {
if(N < 2) return N;
size_t res1, res2;
// spawn the left child as an async runtime task
rt.silent_async([N, &res1](tf::Runtime& rt1) {
res1 = fibonacci(N-1, rt1);
});
// compute the right child inline (tail optimisation)
res2 = fibonacci(N-2, rt);
// cooperatively wait for the left child:
// the worker stays active and local state (res1, res2) is preserved
rt.corun();
return res1 + res2;
}
int main() {
tf::Executor executor;
size_t N = 5, res;
executor.silent_async([N, &res](tf::Runtime& rt) {
res = fibonacci(N, rt);
});
executor.wait_for_all();
std::cout << N << "-th Fibonacci number is " << res << '\n';
return 0;
}
void silent_async(P &&params, F &&func)
similar to tf::Executor::async but does not return a future object
void wait_for_all()
waits for all tasks to complete
void corun()
corun all tasks spawned by this runtime with other workers
Definition runtime.hpp:646

The figure below shows the execution diagram for fibonacci(4). The suffix _1 denotes the left child spawned by its parent runtime:

Note
tf::Runtime::corun can also be called with a taskflow or a graph object to run it cooperatively from within a runtime task. See tf::Runtime::corun for the full set of overloads.