| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a7d3795 commit eefdb8c
13 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,8 +53,8 @@ visit the [documentation][wiki] to learn more about Cpp-Taskflow. | |||
| 53 | 53 | Technical details can be referred to our [IEEE IPDPS19 paper][IPDPS19]. | |
| 54 | 54 | ||
| 55 | 55 | :exclamation: Notice that starting at v2.2.0 (including this master branch) | |
| 56 | - we isolated the executor interface from Taskflow to improve programming model and performance. | ||
| 57 | - This introduced a few breaks in using Cpp-Taskflow. | ||
| 56 | + we isolated the executor interface from Taskflow to improve the programming model and performance. | ||
| 57 | + This caused a few breaks in using Cpp-Taskflow. | ||
| 58 | 58 | Please refer to [release-notes](https://cpp-taskflow.github.io/cpp-taskflow/release-2-2-0.html) | |
| 59 | 59 | for adapting to this new change. | |
| 60 | 60 | ||
@@ -154,28 +154,28 @@ b3.precede(T); // b3 runs before T | |||
| 154 | 154 | ||
| 155 | 155 | # Create a Taskflow Graph | |
| 156 | 156 | ||
| 157 | - Cpp-Taskflow has very expressive and neat methods to create dependency graphs. | ||
| 157 | + Cpp-Taskflow defines a very expressive API to create task dependency graphs. | ||
| 158 | 158 | Most applications are developed through the following three steps. | |
| 159 | 159 | ||
| 160 | 160 | ## Step 1: Create a Task | |
| 161 | 161 | ||
| 162 | - Create a taskflow object to start with a task dependency graph: | ||
| 162 | + Create a taskflow object to build a task dependency graph: | ||
| 163 | 163 | ||
| 164 | 164 | ```cpp | |
| 165 | - tf::Taskflow tf; | ||
| 165 | + tf::Taskflow taskflow; | ||
| 166 | 166 | ``` | |
| 167 | 167 | ||
| 168 | 168 | A task is a callable object for which [std::invoke][std::invoke] is applicable. | |
| 169 | 169 | Use the method `emplace` to create a task: | |
| 170 | 170 | ||
| 171 | 171 | ```cpp | |
| 172 | - tf::Task A = tf.emplace([](){ std::cout << "Task A\n"; }); | ||
| 172 | + tf::Task A = taskflow.emplace([](){ std::cout << "Task A\n"; }); | ||
| 173 | 173 | ``` | |
| 174 | 174 | ||
| 175 | - You can create multiple tasks at one time. | ||
| 175 | + You can create multiple tasks at one time: | ||
| 176 | 176 | ||
| 177 | 177 | ```cpp | |
| 178 | - auto [A, B, C, D] = tf.emplace( | ||
| 178 | + auto [A, B, C, D] = taskflow.emplace( | ||
| 179 | 179 | [] () { std::cout << "Task A\n"; }, | |
| 180 | 180 | [] () { std::cout << "Task B\n"; }, | |
| 181 | 181 | [] () { std::cout << "Task C\n"; }, | |
@@ -185,68 +185,72 @@ auto [A, B, C, D] = tf.emplace( | |||
| 185 | 185 | ||
| 186 | 186 | ## Step 2: Define Task Dependencies | |
| 187 | 187 | ||
| 188 | - Once tasks are created in the pool, you need to specify task dependencies in a | ||
| 189 | - [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph) fashion. | ||
| 188 | + You can add dependency links between tasks to enforce one task run after another. | ||
| 189 | + The dependency links must be specified in a | ||
| 190 | + [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). | ||
| 190 | 191 | The handle `Task` supports different methods for you to describe task dependencies. | |
| 191 | 192 | ||
| 192 | - **Precede**: Adding a preceding link forces one task to run ahead of one another. | ||
| 193 | + **Precede**: Adding a preceding link forces one task to run before another. | ||
| 193 | 194 | ```cpp | |
| 194 | 195 | A.precede(B); // A runs before B. | |
| 195 | 196 | ``` | |
| 196 | 197 | ||
| 197 | - **Gather**: Adding a gathering link forces one task to run after other(s). | ||
| 198 | + **Gather**: Adding a gathering link forces one task to run after another. | ||
| 198 | 199 | ```cpp | |
| 199 | 200 | A.gather(B); // A runs after B | |
| 200 | 201 | ``` | |
| 201 | 202 | ||
| 202 | 203 | ## Step 3: Execute a Taskflow | |
| 203 | 204 | ||
| 204 | - To execute a taskflow, you need to create an executor. | ||
| 205 | - An executor manages a set of worker threads to execute each dependent tasks | ||
| 206 | - through an efficient work-stealing algorithm. | ||
| 205 | + To execute a taskflow, you need to create an *executor*. | ||
| 206 | + An executor manages a set of worker threads to execute a taskflow | ||
| 207 | + through an efficient *work-stealing* algorithm. | ||
| 207 | 208 | ||
| 208 | 209 | ```cpp | |
| 209 | - tf::Executor executor; // executor manages a set of worker threads | ||
| 210 | + tf::Executor executor; | ||
| 210 | 211 | ``` | |
| 211 | 212 | ||
| 212 | - The executor class provides a rich set of methods to run a taskflow. | ||
| 213 | - You can run a taskflow multiple times or until a stopping criteria is met. | ||
| 214 | - These methods are non-blocking and will return a [std::shared_future][std::shared_future] | ||
| 213 | + The executor provides a rich set of methods to run a taskflow. | ||
| 214 | + You can run a taskflow one or multiple times, or until a stopping criteria is met. | ||
| 215 | + These methods are non-blocking and all return a [std::shared_future][std::shared_future] | ||
| 215 | 216 | to let you query the execution status. | |
| 216 | 217 | ||
| 217 | 218 | ```cpp | |
| 218 | - executor.run(tf); // run the tf once | ||
| 219 | - executor.run(tf, [](){ std::cout << "done 1 run\n"; } ); // run once with a callback | ||
| 220 | - executor.run_n(tf, 4); // run four times | ||
| 221 | - executor.run_n(tf, 4, [](){ std::cout << "done 4 runs\n"; }); // run 4 times with a callback | ||
| 219 | + executor.run(taskflow); // run the taskflow once | ||
| 220 | + executor.run(taskflow, [](){ std::cout << "done 1 run\n"; } ); // run once with a callback | ||
| 221 | + executor.run_n(taskflow, 4); // run four times | ||
| 222 | + executor.run_n(taskflow, 4, [](){ std::cout << "done 4 runs\n"; }); // run 4 times with a callback | ||
| 222 | 223 | ||
| 223 | 224 | // run n times until the predicate becomes true | |
| 224 | - executor.run_until(tf, [counter=4](){ return --counter == 0; } ); | ||
| 225 | + executor.run_until(taskflow, [counter=4](){ return --counter == 0; } ); | ||
| 225 | 226 | ||
| 226 | 227 | // run n times until the predicate becomes true and invoke the callback on completion | |
| 227 | - executor.run_until(tf, [counter=4](){ return --counter == 0; }, | ||
| 228 | - [](){ std::cout << "Execution finishes\n"; } ); | ||
| 228 | + executor.run_until(taskflow, [counter=4](){ return --counter == 0; }, | ||
| 229 | + [](){ std::cout << "Execution finishes\n"; } ); | ||
| 229 | 230 | ``` | |
| 230 | 231 | ||
| 231 | 232 | ||
| 232 | - You can call `wait_for_all` to block the executor until all associated tasks complete. | ||
| 233 | + You can call `wait_for_all` to block the executor until all associated taskflows complete. | ||
| 233 | 234 | ||
| 234 | 235 | ```cpp | |
| 235 | 236 | executor.wait_for_all(); // block until all associated tasks finish | |
| 236 | 237 | ``` | |
| 237 | 238 | ||
| 238 | 239 | Notice that executor does not own any taskflow. | |
| 239 | - It is your responsibility to keep a taskflow alive during its execution. | ||
| 240 | + It is your responsibility to keep a taskflow alive during its execution, | ||
| 241 | + or it can result in undefined behavior. | ||
| 242 | + In most applications, you need only one executor to run multiple taskflows | ||
| 243 | + each representing a specific part of your parallel decomposition. | ||
| 240 | 244 | ||
| 241 | 245 | # Dynamic Tasking | |
| 242 | 246 | ||
| 243 | 247 | Another powerful feature of Taskflow is *dynamic* tasking. | |
| 244 | - A dynamic task is created during the execution of a taskflow. | ||
| 248 | + Dynamic tasks are those created during the execution of a taskflow. | ||
| 245 | 249 | These tasks are spawned by a parent task and are grouped together to a *subflow* graph. | |
| 246 | 250 | The example below demonstrates how to create a subflow | |
| 247 | 251 | that spawns three tasks at runtime. | |
| 248 | 252 | ||
| 249 | - <img align="right" src="image/subflow_join.png" width="35%"> | ||
| 253 | + <img align="right" src="image/subflow_join.png" width="30%"> | ||
| 250 | 254 | ||
| 251 | 255 | ```cpp | |
| 252 | 256 | // create three regular tasks | |
@@ -273,7 +277,7 @@ By default, a subflow graph joins to its parent node. | |||
| 273 | 277 | This guarantees a subflow graph to finish before the successors of | |
| 274 | 278 | its parent node. | |
| 275 | 279 | You can disable this feature by calling `subflow.detach()`. | |
| 276 | - Detaching the above subflow will result in the following execution flow. | ||
| 280 | + For example, detaching the above subflow will result in the following execution flow: | ||
| 277 | 281 | ||
| 278 | 282 | <img align="right" src="image/subflow_detach.png" width="35%"> | |
| 279 | 283 | ||
@@ -295,16 +299,16 @@ tf::Task B = tf.emplace([] (tf::SubflowBuilder& subflow) { | |||
| 295 | 299 | ||
| 296 | 300 | Cpp-Taskflow has an unified interface for static and dynamic tasking. | |
| 297 | 301 | To create a subflow for dynamic tasking, | |
| 298 | - emplace a callable on one argument of type `tf::SubflowBuilder`. | ||
| 302 | + emplace a callable with one argument of type `tf::SubflowBuilder`. | ||
| 299 | 303 | ||
| 300 | 304 | ```cpp | |
| 301 | 305 | tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) {}); | |
| 302 | 306 | ``` | |
| 303 | 307 | ||
| 304 | 308 | A subflow builder is a lightweight object that allows you to create | |
| 305 | - arbitrary dependency graphs on the fly. | ||
| 309 | + arbitrary dependency graphs at runtime. | ||
| 306 | 310 | All graph building methods defined in taskflow | |
| 307 | - can be used in a subflow builder. | ||
| 311 | + can be used in the subflow builder. | ||
| 308 | 312 | ||
| 309 | 313 | ```cpp | |
| 310 | 314 | tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) { | |
@@ -317,7 +321,7 @@ tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) { | |||
| 317 | 321 | }); | |
| 318 | 322 | ``` | |
| 319 | 323 | ||
| 320 | - A subflow can also be nested or recursive. You can create another subflow from | ||
| 324 | + A subflow can be nested or recursive. You can create another subflow from | ||
| 321 | 325 | the execution of a subflow and so on. | |
| 322 | 326 | ||
| 323 | 327 | <img align="right" src="image/nested_subflow.png" width="25%"> | |
@@ -356,8 +360,8 @@ tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) { | |||
| 356 | 360 | }); // subflow starts to run after the callable scope | |
| 357 | 361 | ``` | |
| 358 | 362 | ||
| 359 | - Detaching or Joining a subflow has different meaning in the ready status of | ||
| 360 | - the future object referred to it. | ||
| 363 | + Detaching or joining a subflow has different meaning in the completion status of | ||
| 364 | + its parent node. | ||
| 361 | 365 | In a joined subflow, | |
| 362 | 366 | the completion of its parent node is defined as when all tasks | |
| 363 | 367 | inside the subflow (possibly nested) finish. | |
@@ -439,8 +443,8 @@ f2B.precede(f1_module_task); | |||
| 439 | 443 | f1_module_task.precede(f2C); | |
| 440 | 444 | ``` | |
| 441 | 445 | ||
| 442 | - The `composed_of` method returns a *module task* and you can use the `precede` and `gather` | ||
| 443 | - methods to define its dependencies. | ||
| 446 | + Similarly, `composed_of` returns a task handle and you can use the same methods | ||
| 447 | + `precede` and `gather` to create dependencies. | ||
| 444 | 448 | You can compose a taskflow from multiple taskflows and use the result | |
| 445 | 449 | to compose another taskflow and so on. | |
| 446 | 450 | ||
@@ -463,19 +467,19 @@ You can dump it to a GraphViz format using the method `dump`. | |||
| 463 | 467 | ||
| 464 | 468 | ```cpp | |
| 465 | 469 | // debug.cpp | |
| 466 | - tf::Taskflow tf; | ||
| 470 | + tf::Taskflow taskflow; | ||
| 467 | 471 | ||
| 468 | - tf::Task A = tf.emplace([] () {}).name("A"); | ||
| 469 | - tf::Task B = tf.emplace([] () {}).name("B"); | ||
| 470 | - tf::Task C = tf.emplace([] () {}).name("C"); | ||
| 471 | - tf::Task D = tf.emplace([] () {}).name("D"); | ||
| 472 | - tf::Task E = tf.emplace([] () {}).name("E"); | ||
| 472 | + tf::Task A = taskflow.emplace([] () {}).name("A"); | ||
| 473 | + tf::Task B = taskflow.emplace([] () {}).name("B"); | ||
| 474 | + tf::Task C = taskflow.emplace([] () {}).name("C"); | ||
| 475 | + tf::Task D = taskflow.emplace([] () {}).name("D"); | ||
| 476 | + tf::Task E = taskflow.emplace([] () {}).name("E"); | ||
| 473 | 477 | ||
| 474 | 478 | A.precede(B, C, E); | |
| 475 | 479 | C.precede(D); | |
| 476 | 480 | B.precede(D, E); | |
| 477 | 481 | ||
| 478 | - tf.dump(std::cout); | ||
| 482 | + taskflow.dump(std::cout); | ||
| 479 | 483 | ``` | |
| 480 | 484 | ||
| 481 | 485 | Run the program and inspect whether dependencies are expressed in the right way. | |
@@ -561,7 +565,7 @@ You can pan or zoom in/out the timeline to get into a detailed view. | |||
| 561 | 565 | ||
| 562 | 566 | # API Reference | |
| 563 | 567 | ||
| 564 | - The official [documentation][wiki] explains the complete list of | ||
| 568 | + The official [documentation][wiki] explains a complete list of | ||
| 565 | 569 | Cpp-Taskflow API. | |
| 566 | 570 | In this section, we highlight a short list of commonly used methods. | |
| 567 | 571 | ||
@@ -583,7 +587,7 @@ The table below summarizes a list of commonly used methods. | |||
| 583 | 587 | ||
| 584 | 588 | ### *emplace/placeholder* | |
| 585 | 589 | ||
| 586 | - You can use `emplace` to create a task for a target callable. | ||
| 590 | + You can use `emplace` to create a task from a target callable. | ||
| 587 | 591 | ||
| 588 | 592 | ```cpp | |
| 589 | 593 | // create a task through emplace | |
@@ -644,7 +648,7 @@ auto [S, T] = tf.parallel_for( | |||
| 644 | 648 | By default, taskflow performs an even partition over worker threads | |
| 645 | 649 | if the group size is not specified (or equal to 0). | |
| 646 | 650 | ||
| 647 | - In addition to range-based iterator, parallel\_for has another overload on an index-based loop. | ||
| 651 | + In addition to range-based iterator, `parallel_for` has another overload of index-based loop. | ||
| 648 | 652 | The first three argument to this overload indicates | |
| 649 | 653 | starting index, ending index (exclusive), and step size. | |
| 650 | 654 | ||
@@ -678,7 +682,7 @@ auto [S, T] = tf.parallel_for( | |||
| 678 | 682 | ||
| 679 | 683 | The method `reduce` creates a subgraph that applies a binary operator to a range of items. | |
| 680 | 684 | The result will be stored in the referenced `res` object passed to the method. | |
| 681 | - It is your responsibility to assign it a correct initial value to reduce. | ||
| 685 | + It is your responsibility to assign a correct initial value to the reduce call. | ||
| 682 | 686 | ||
| 683 | 687 | <img align="right" width="45%" src="image/reduce.png"> | |
| 684 | 688 | ||
@@ -702,15 +706,15 @@ auto [S, T] = tf.transform_reduce(v.begin(), v.end(), min, | |||
| 702 | 706 | ); | |
| 703 | 707 | ``` | |
| 704 | 708 | ||
| 705 | - By default, all reduce methods distribute the workload evenly across threads. | ||
| 709 | + By default, all reduce methods distribute the workload evenly across `std::thread::hardware_concurrency`. | ||
| 706 | 710 | ||
| 707 | 711 | ## Task API | |
| 708 | 712 | ||
| 709 | 713 | Each time you create a task, the taskflow object adds a node to the present task dependency graph | |
| 710 | 714 | and return a *task handle* to you. | |
| 711 | 715 | A task handle is a lightweight object that defines a set of methods for users to | |
| 712 | 716 | access and modify the attributes of the associated task. | |
| 713 | - The table below summarizes the list of commonly used methods. | ||
| 717 | + The table below summarizes a list of commonly used methods. | ||
| 714 | 718 | ||
| 715 | 719 | | Method | Argument | Return | Description | | |
| 716 | 720 | | -------------- | ----------- | ------ | ----------- | | |
@@ -739,7 +743,7 @@ A.work([] () { std::cout << "hello world!"; }); | |||
| 739 | 743 | ||
| 740 | 744 | ### *precede* | |
| 741 | 745 | ||
| 742 | - The method `precede` is the basic building block to add a precedence between two tasks. | ||
| 746 | + The method `precede` lets you add a preceding link from self to a task. | ||
| 743 | 747 | ||
| 744 | 748 | <img align="right" width="20%" src="image/precede.png"> | |
| 745 | 749 | ||
@@ -760,7 +764,7 @@ A.precede(B, C, D, E); | |||
| 760 | 764 | ||
| 761 | 765 | ### *gather* | |
| 762 | 766 | ||
| 763 | - The method `gather` lets you add multiple precedences to a task. | ||
| 767 | + The method `gather` lets you add a preceding link from a task to self. | ||
| 764 | 768 | ||
| 765 | 769 | <img align="right" width="30%" src="image/gather.png"> | |
| 766 | 770 | ||
@@ -785,33 +789,38 @@ The table below summarizes a list of commonly used methods. | |||
| 785 | 789 | ||
| 786 | 790 | ### *Executor* | |
| 787 | 791 | ||
| 788 | - The constructor of tf::Executor can take an unsigned integer to create N worker threads. | ||
| 792 | + The constructor of tf::Executor takes an unsigned integer to | ||
| 793 | + initialize the executor with `N` worker threads. | ||
| 789 | 794 | ||
| 790 | 795 | ```cpp | |
| 791 | 796 | tf::Executor executor(8); // create an executor of 8 worker threads | |
| 792 | 797 | ``` | |
| 793 | 798 | ||
| 794 | - By default, we use `std::thread::hardware_concurrency` to decide the number of worker threads. | ||
| 799 | + The default value uses `std::thread::hardware_concurrency` | ||
| 800 | + to decide the number of worker threads. | ||
| 795 | 801 | ||
| 796 | 802 | ### *run/run_n/run_until* | |
| 797 | 803 | ||
| 798 | 804 | The run series are non-blocking call to execute a taskflow graph. | |
| 799 | - Issuing multiple runs on the same taskflow will automatically synchronize the execution. | ||
| 805 | + Issuing multiple runs on the same taskflow will automatically synchronize | ||
| 806 | + to a sequential chain of executions. | ||
| 800 | 807 | ||
| 801 | 808 | ```cpp | |
| 802 | 809 | executor.run(taskflow); // run a graph once | |
| 803 | 810 | executor.run_n(taskflow, 5); // run a graph five times | |
| 804 | 811 | executor.run_n(taskflow, my_pred); // keep running a graph until the predicate becomes true | |
| 805 | 812 | ``` | |
| 806 | 813 | ||
| 814 | + The first run finishes before the second run, and the second run finishes before the third run. | ||
| 815 | + | ||
| 807 | 816 | # Caveats | |
| 808 | 817 | ||
| 809 | 818 | While Cpp-Taskflow enables the expression of very complex task dependency graph that might contain | |
| 810 | 819 | thousands of task nodes and links, there are a few amateur pitfalls and mistakes to be aware of. | |
| 811 | 820 | ||
| 812 | 821 | + Having a cycle in a graph may result in running forever | |
| 822 | + + Destructing a taskflow while it is running in one execution results in undefined behavior | ||
| 813 | 823 | + Trying to modify a running task can result in undefined behavior | |
| 814 | - + Touching a taskflow from multiple threads are not safe | ||
| 815 | 824 | ||
| 816 | 825 | Cpp-Taskflow is known to work on Linux distributions, MAC OSX, and Microsoft Visual Studio. | |
| 817 | 826 | Please [let me know][email me] if you found any issues in a particular platform. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments