namespace tf { /** @page StaticTasking Static Tasking This chapter demonstrates how to create a static task dependency graph. Static tasking captures the static parallel structure of a decomposition and is defined only by the program itself. It has a flat task hierarchy and cannot spawn new tasks from a running dependency graph. @tableofcontents @section CreateATaskDependencyGraph Create a Task Dependency Graph A task in %Taskflow is a @em 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 @c 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. @code{.cpp} 1: tf::Taskflow taskflow; 2: tf::Task A = taskflow.placeholder(); 3: tf::Task B = taskflow.emplace([] () { std::cout << "task B\n"; }); 4: 5: auto [D, E, F] = taskflow.emplace( 6: [](){ std::cout << "Task A\n"; }, 7: [](){ std::cout << "Task B\n"; }, 8: [](){ std::cout << "Task C\n"; } 9: ); @endcode Debrief: @li Line 1 creates a taskflow object, or a @em graph @li Line 2 creates a placeholder task without work (i.e., callable) @li Line 3 creates a task from a given callable object and returns a task handle @li Lines 5-9 create three tasks in one call using C++ structured binding coupled with std::tuple Each time you create a task, the taskflow object creates a node in the task graph and returns a task handle of type tf::Task. A task handle is a lightweight object that wraps up a particular node in a graph and provides a set of methods for you to assign different attributes to the task such as adding dependencies, naming, and assigning a new work. @code{.cpp} 1: tf::Taskflow taskflow; 2: tf::Task A = taskflow.emplace([] () { std::cout << "create a task A\n"; }); 3: tf::Task B = taskflow.emplace([] () { std::cout << "create a task B\n"; }); 4: 5: A.name("TaskA"); 6: A.work([] () { std::cout << "reassign A to a new callable\n"; }); 7: A.precede(B); 8: 9: std::cout << A.name() << std::endl; // TaskA 10: std::cout << A.num_successors() << std::endl; // 1 11: std::cout << A.num_dependents() << std::endl; // 0 12: 13: std::cout << B.num_successors() << std::endl; // 0 14: std::cout << B.num_dependents() << std::endl; // 1 @endcode Debrief: @li Line 1 creates a taskflow object @li Lines 2-3 create two tasks A and B @li Lines 5-6 assign a name and a work to task A, and add a precedence link to task B @li Line 7 adds a dependency link from A to B @li Lines 9-14 dump the task attributes %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: @code{.cpp} taskflow.emplace([ptr=std::make_unique(1)](){ std::cout << "captured unique pointer is not copyable"; }); @endcode @section VisualizeATaskDependencyGraph Visualize a Task Dependency Graph You can dump a taskflow to a DOT format and visualize the graph using free online tools such as GraphvizOnline and WebGraphviz. @code{.cpp} 1: #include 2: 3: int main() { 4: 5: tf::Taskflow taskflow; 6: 7: // create a task dependency graph 8: tf::Task A = taskflow.emplace([] () { std::cout << "Task A\n"; }); 9: tf::Task B = taskflow.emplace([] () { std::cout << "Task B\n"; }); 10: tf::Task C = taskflow.emplace([] () { std::cout << "Task C\n"; }); 11: tf::Task D = taskflow.emplace([] () { std::cout << "Task D\n"; }); 12: 13: // add dependency links 14: A.precede(B); 15: A.precede(C); 16: B.precede(D); 17: C.precede(D); 18: 19: taskflow.dump(std::cout); 20: } @endcode Debrief: @li Line 5 creates a taskflow object @li Lines 8-11 create four tasks @li Lines 14-17 add four task dependencies @li Line 19 dumps the taskflow in the DOT format through standard output @dotfile images/simple.dot @section ModifyTaskAttributes Modify Task Attributes This example demonstrates how to modify a task's attributes using methods defined in the task handler. @code{.cpp} 1: #include 2: 3: int main() { 4: 5: tf::Taskflow taskflow; 6: 7: std::vector tasks = { 8: taskflow.placeholder(), // create a task with no work 9: taskflow.placeholder() // create a task with no work 10: }; 11: 12: tasks[0].name("This is Task 0"); 13: tasks[1].name("This is Task 1"); 14: tasks[0].precede(tasks[1]); 15: 16: for(auto task : tasks) { // print out each task's attributes 17: std::cout << task.name() << ": " 18: << "num_dependents=" << task.num_dependents() << ", " 19: << "num_successors=" << task.num_successors() << '\n'; 20: } 21: 22: taskflow.dump(std::cout); // dump the taskflow graph 23: 24: tasks[0].work([](){ std::cout << "got a new work!\n"; }); 25: tasks[1].work([](){ std::cout << "got a new work!\n"; }); 26: 27: return 0; 28: } @endcode The output of this program looks like the following: @code{.sh} This is Task 0: num_dependents=0, num_successors=1 This is Task 1: num_dependents=1, num_successors=0 digraph Taskflow { "This is Task 1"; "This is Task 0"; "This is Task 0" -> "This is Task 1"; } @endcode Debrief: @li Line 5 creates a taskflow object @li Lines 7-10 create two placeholder tasks with no works and stores the corresponding task handles in a vector @li Lines 12-13 name the two tasks with human-readable strings @li Line 14 adds a dependency link from the first task to the second task @li Lines 16-20 print out the name of each task, the number of dependents, and the number of successors @li Line 22 dumps the task dependency graph to a @GraphVizOnline format (dot) @li Lines 24-25 assign a new target to each task You can change the name and work of a task at anytime before running the graph. The later assignment overwrites the previous values. @section TraverseAdjacentTasks Traverse Adjacent Tasks You can iterate the successor list and the dependent list of a task by using tf::Task::for_each_successor and tf::Task::for_each_dependent, respectively. Each method takes a lambda and applies it to a successor or a dependent being traversed. @code{.cpp} // traverse all successors of my_task my_task.for_each_successor([s=0] (tf::Task successor) mutable { std::cout << "successor " << s++ << '\n'; }); // traverse all dependents of my_task my_task.for_each_dependent([d=0] (tf::Task dependent) mutable { std::cout << "dependent " << d++ << '\n'; }); @endcode @section AttachUserDataToATask Attach User Data to a Task 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. @code{.cpp} int my_data = 5; tf::Task task = taskflow.placeholder(); task.data(&my_data) .work([task](){ int my_date = *static_cast(task.data()); std::cout << "my_data: " << my_data; }); @endcode 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. @attention It is your responsibility to ensure that the attached data stay alive during the execution of its task. @section UnderstandTheLifetimeOfATask Understand the Lifetime of a Task A task lives with its graph and belongs to only a graph at a time, and is not destroyed until the graph gets cleaned up. The lifetime of a task refers to the user-given callable object, including captured values. As long as the graph is alive, all the associated tasks exist. @attention It is your responsibility to keep tasks and graph alive during their execution. @section MoveATaskflow Move a Taskflow You can construct or assign a taskflow from a @em moved taskflow. Moving a taskflow to another will result in transferring the underlying graph data structures from one to the other. @code{.cpp} tf::Taskflow taskflow1, taskflow3; taskflow1.emplace([](){}); // move-construct taskflow2 from taskflow1 tf::Taskflow taskflow2(std::move(taskflow1)); assert(taskflow2.num_tasks() == 1 && taskflow1.num_tasks() == 0); // move-assign taskflow3 to taskflow2 taskflow3 = std::move(taskflow2); assert(taskflow3.num_tasks() == 1 && taskflow2.num_tasks() == 0); @endcode You can only move a taskflow to another while that taskflow is not being run by an executor. Moving a running taskflow can result in undefined behavior. Please see @ref ExecuteATaskflowWithTransferredOwnership for more details. */ }