Cpp-Taskflow  2.1.0
C7: Framework (Experimental)

In many situations, you will need to execute a task dependency multiple times to create a reusable and stateful execution flow. tf::Framework is designed for this purpose. This chapter introduces the concept of a framework and shows its usage.

Create a Framework

The tf::Framework inherits tf::FlowBuilder and contains a task dependency graph that is independent of a taskflow object. The big difference from dispatching a graph from taskflow is executing a framework will not make the graph disappear. You can reuse the framework multiple times. The following example demonstrates how to create a framework.

// createe a framework
tf::Framework framework;
// create three tasks in the framework
tf::Task A = framework.emplace([](){ std::cout << "This is TaskA\n"; });
tf::Task B = framework.placeholder();
// add a dependency link from A to B
A.precede(B);
// build precedence links from A to B and C
A.precede(B, C);

Execute a Framework

To execute a framework, you use one of following methods: tf::Taskflow::run, tf::Taskflow::run_n, or tf::Taskflow::run_until to choose to run a framework for one time, multiple times, or until reaching a certain condition. All methods accept an optional callback to invoke after the execution completes. The code below shows how to run a framework.

1: // Declare a framework
3:
4: // Add three tasks into the framework
5: auto [A, B, C] = f.emplace(
6: [] () { std::cout << "This is TaskA\n"; },
7: [] () { std::cout << "This is TaskB\n"; },
8: [] () { std::cout << "This is TaskC\n"; },
9: );
10:
11: // Build precedence between tasks
12: A.precede(B, C);
13:
14: // Declare a taskflow object
15: tf::Taskflow taskflow;
16:
17: auto fu = taskflow.run(f);
18: fu.get();
19: taskflow.run(f, [](){ std::cout << "end of one execution\n"; }).get();
20:
21: taskflow.run_n(f, 4);
22: taskflow.wait_for_all();
23:
24: taskflow.run_n(f, 4, [](){ std::cout << "end of four executions\n"; }).get();
25:
26: taskflow.run_until(f, [int cnt=0] () mutable { return (++cnt == 10); });

Debrief:

  • Line 1-12 creates a framework of three tasks A, B, and C
  • Line 15 creates a taskflow object as the executor
  • Line 17-18 runs the framework once and use std::shared_future::get to wait for completion
  • Line 19 runs the framework once with a callback to invoke when the execution finishes
  • Line 21-22 runs the framework four times and use tf::Taskflow::wait_for_all to wait for completion
  • Line 24 runs the framework four times and invokes a callback at the end of the forth execution
  • Line 26 keeps running the framework until the cnt variable becomes 10

Lifetime of a Framework

Since a taskflow object does not own a framework, a running framework must remain alive until the execution finishes. That is, it is your responsibility to ensure a framework object retain its life during it is running on a taskflow object. To make sure all frameworks have completed, you can use tf::Taskflow::wait_for_all to block the program until all tasks finish.

// create a taskflow object
tf::Taskflow taskflow;
// create a framework whose lifetime is restricted by the scope
{
// add tasks into the framework
// ...
// run the framework
taskflow.run(f);
} // destroy the framework without waiting for execution finishes will lead to undefined behavior

Create an Application Framework

A useful feature of framework is that you can customize your own application framework by inheriting the tf::Framework class. By deriving from the tf::Framework, you can use the same task creation APIs to build a task dependency graph for your own application and call run_* methods to execute your framework.

// Define a framework for your application
class Foo: public tf::Framework {
// Define members data and functions
void set_inputs(std::vector<float>&);
// ...
};
int main() {
// Declare a taskflow object
tf::Taskflow taskflow;
// Declare an application framework
Foo foo;
// Use the task creation APIs to build the task dependency graph in application framework
tf::Task taskA = foo.emplace([](){ std::cout << "TaskA\n"; });
tf::Task taskB = foo.emplace([](){ std::cout << "TaskB\n"; });
taskA.precede(taskB);
// Dispatch your application framework
taskflow.run(foo);
taskflow.wait_for_all();
return 0;
}

Caveats

Although tf::Framework enables efficient reuse of a task dependency graph, there can be many potential pitfalls. First, a framework object is NOT thread-safe. Trying to touch a framework while it is running can result in undefined behavior.

// Add tasks into the framework
// ...
// Declare a taskflow object
tf::Taskflow taskflow;
auto future = taskflow.run(f);
// Modify the framework before the execution finishes leads to undefined behavior
f.emplace([](){ std::cout << "Add a new task\n"; });
// Use get method to wait for the execution completes.
future.get();

Second, subgraphs spawned during dynamic tasking will be cleaned up automatically at the beginning of each execution of the framework.

tf::Task A = f.emplace([](){ std::cout << "TaskA" << std::endl; });
tf::Task B = f.emplace([](tf::SubflowBuilder& subflow){
std::cout << "TaskB" << std::endl;
subflow.emplace([](){ std::cout << "SubTask" << std::endl; });
});
A.precede(B);
// Declare a taskflow object
tf::Taskflow taskflow;
std::cout << "First run:" << std::endl;
taskflow.run(f).get();
// Previous spawn subflow is removed before the second execution, only one SubTask message will be printed.
std::cout << "Second run:" << std::endl;
taskflow.run(f).get();

Third, connecting the tasks from different frameworks can result in undefined behaviors.

tf::Taskflow taskflow;
tf::Task A = f1.emplace([] () { std::cout << "task of framework 1\n"; });
tf::Task B = f2.emplace([] () { std::cout << "task of framework 2\n"; });
// connecting the tasks from different frameworks results in undefined behaviors
A.precede(B);
taskflow.run(f1);

We are still experimenting tf::Framework to develop a safe interface. Please stay tuned with Master Branch (GitHub).