Static tasking is the most basic programming model in Taskflow. It follows the construct-and-run model, where you define a taskflow graph first and submit it to an executor for execution.
A task in Taskflow is a callable object for which the operation std::invoke is applicable. It can be either a functor, a lambda expression, a bind expression, or a class objects with operator() overloaded. All tasks are created from tf::Taskflow, the class that manages a task dependency graph. Taskflow provides two methods, tf::Taskflow::placeholder and tf::Taskflow::emplace to create a task.
For example, the code below creates a taskflow. It first defines a placeholder task without assigned work, then creates a task directly from a given callable and obtains its task handle. Finally, it creates multiple tasks in one call using C++ structured binding.
Each time you create a task, the taskflow creates a node and returns a task handle of type tf::Task. A task handle is a copy-cheap wrapper over the node pointer to a task in taskflow. The handle provides a set of methods for you to access and modify the attributes of a task, such as building dependencies, assigning a name, changing the work, querying task statistics, and so on.
The code above creates a taskflow of two tasks A and B. It then assigns a name and a new callable to task A, and establishes a precedence link to task B. Finally, it queries the task attributes, including names, successor counts, and predecessor counts of A and B.
Taskflow uses general-purpose polymorphic function wrapper, std::function, to store and invoke a callable in a task. You need to follow its contract to create a task. For example, the callable to construct a task must be copyable, and thus the code below won't compile:
You can dump a taskflow to a DOT format and visualize the graph using free online tools such as GraphvizOnline and WebGraphviz. For example, the code below dumps the taskflow through the standard output std::cout.
Visualization helps you understand how tasks and dependencies are structured, making it easier to analyze and debug your taskflow programs.
You can iterate the successor list and the predecessor list of a task by using tf::Task::for_each_successor and tf::Task::for_each_predecessor, respectively. Both methods take a unary function that takes an argument of type tf::Task pointing to the task that is being visited.
Together with tf::Taskflow::for_each_task, you can traverse a taskflow graph. For example, the code below traverse a taskflow and outputs the successor and the predecessor information of each task:
If the task contains a subflow (see Subflow Tasking), you can use tf::Task::for_each_subflow_task to iterate all tasks associated with that subflow.
You can attach custom data to a task using tf::Task::data(void*) and access it using tf::Task::data(). Each node in a taskflow is associated with a C-styled data pointer (i.e., void*) you can use to point to user data and access it in the body of a task callable. The following example attaches an integer to a task and accesses that integer through capturing the data in the callable.
Notice that you need to create a placeholder task first before assigning it a work callable. Only this way can you capture that task in the lambda and access its attached data in the lambda body. Also, as Taskflow does not manage any user data, it is your responsibility to ensure any attached data stays alive during the??
A task belongs to a single graph at a time and remains alive as long as that graph exists. The lifetime of a task is particularly important when referring to its callable, including any captured values. When the graph is destroyed or cleaned up, all associated tasks are also destroyed. Consequently, it is your responsibility to keep relevant taskflows alive during their execution. For example, the code below can crash because the taskflow may be destroyed before the executor finishes running it, leaving the executor with dangling references to the task graph.
You can construct or assign a taskflow using C++ move semantics. Moving a taskflow to another will result in transferring the underlying graph data structures from one to the other.
You can only move a taskflow to another taskflow when it is not being used, such as being executed by an executor. Moving a taskflow that is being used may result in undefined behavior.