Cookbook » Runtime Tasking

Taskflow allows you to interact with the scheduling runtime from the execution context of a runtime task. Runtime tasking is mostly used for designing specialized parallel algorithms that go beyond the default scheduling rules of taskflows.

Create a Runtime Task

A runtime task is a callable that takes a reference to a tf::Runtime object in its argument. A tf::Runtime object is created by the running executor and contains several methods for users to interact with the scheduling runtime. For instance, the following code creates a runtime task to forcefully schedule a conditioned task that would never happens:

tf::Task A, B, C, D;
std::tie(A, B, C, D) = taskflow.emplace(
  [] () { return 0; },
  [&C] (tf::Runtime& rt) {  // C must be captured by reference
    std::cout << "B\n"; 
    rt.schedule(C);
  },
  [] () { std::cout << "C\n"; },
  [] () { std::cout << "D\n"; }
);
A.precede(B, C, D);
executor.run(taskflow).wait();
Taskflow p0x7bc400014030 D p0x7bc400014118 C p0x7bc400014200 B p0x7bc4000142e8 A p0x7bc4000142e8->p0x7bc400014030 2 p0x7bc4000142e8->p0x7bc400014118 1 p0x7bc4000142e8->p0x7bc400014200 0

When the condition task A completes and returns 0, the scheduler moves on to the runtime task B. Under the normal circumstance, tasks C and D will not run because their conditional dependencies never happen. This can be broken by forcefully scheduling C or/and D via a runtime task that resides in the same graph. Here, the runtime task B call tf::Runtime::schedule(tf::Task) to run task C even though the weak dependency between A and C will never happen based on the graph structure itself. As a result, we will see both B and C in the output:

B    # B is a runtime task to schedule C out of its dependency constraint
C

Acquire the Running Executor

You can acquire the reference to the running executor using tf::Runtime::executor(). The running executor of a runtime task is the executor that runs the parent taskflow of that runtime task.

tf::Executor executor;
tf::Taskflow taskflow;
taskflow.emplace([&](tf::Runtime& rt){
  assert(&(rt.executor()) == &executor);
});
executor.run(taskflow).wait();

Run a Task Graph Synchronously

A runtime task can spawn and run a task graph synchronously using tf::Runtime::run_and_wait. This model allows you to leverage dynamic tasking to execute a parallel workload within a runtime task. The following code creates a subflow of two independent tasks and executes it synchronously via the given runtime task:

taskflow.emplace([](tf::Runtime& rt){
  rt.run_and_wait([](tf::Subflow& sf){
    sf.emplace([](){ std::cout << "independent task 1\n"; });
    sf.emplace([](){ std::cout << "independent task 2\n"; });
  });
  // subflow joins upon run_and_wait returns
});

You can also create a task graph yourself and execute it through a runtime task. This organization avoids repetitive creation of a subflow with the same topology, such as running a runtime task repetitively. The following code performs the same execution logic as the above example but using the given task graph to avoid repetitive creations of a subflow:

// create a custom graph
tf::Taskflow graph;
graph.emplace([](){ std::cout << "independent task 1\n"; });
graph.emplace([](){ std::cout << "independent task 2\n"; });

taskflow.emplace([&](tf::Runtime& rt){ 
  rt.run_and_wait(graph);  // this worker thread continues the work-stealing loop
});
executor.run_n(taskflow, 10000);

Although tf::Runtime::run_and_wait blocks until the operation completes, the caller thread (worker) is not blocked (e.g., sleep or holding any lock). Instead, the caller thread joins the work-stealing loop of the executor and returns whenever the spawned task graph completes. This is different from waiting for a submitted taskflow to finish (executor.run(taskflow).wait()) which blocks the caller thread until the submitted taskflow completes. When multiple submitted taskflows are being waited, their executions can potentially lead to deadlock. Using tf::Runtime::run_and_wait avoids the deadlock problem.