Skip to content

Commit 7bf77f5

Browse files
modified doc
1 parent 2cb3884 commit 7bf77f5

12 files changed

Lines changed: 780 additions & 57 deletions

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ std::cout << tf.dump_topologies();
443443
444444
The class `tf::Taskflow` is the main place to create taskflow graphs and carry out task dependencies.
445445
The table below summarizes a list of commonly used methods.
446+
Visit [documentation][wiki] to see the complete list.
446447
447448
| Method | Argument | Return | Description |
448449
| -------- | --------- | ------- | ----------- |
@@ -613,7 +614,8 @@ std::cout << "all topologies complete" << '\n';
613614
## Task API
614615

615616
Each task object is a lightweight handle for you to create dependencies in its associated graph.
616-
The table below summarizes its methods.
617+
The table below summarizes the list of commonly used methods.
618+
Visit [documentation][wiki] to see the complete list.
617619

618620
| Method | Argument | Return | Description |
619621
| -------------- | ----------- | ------ | ----------- |
@@ -736,7 +738,7 @@ The folder `example/` contains several examples and is a great place to learn to
736738
# Get Involved
737739
+ Report bugs/issues by submitting a [GitHub issue][GitHub issues]
738740
+ Submit contributions using [pull requests][GitHub pull requests]
739-
+ Learn more about Cpp-Taskflow by visitng our [wiki][wiki]
741+
+ Learn more about Cpp-Taskflow by reading the [documentation][wiki]
740742
+ Find a solution to your question at [Frequently Asked Questions][FAQ]
741743

742744
# Contributors

doc/examples/dynamic_tasking.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Spawn a Task Dependency Graph at Runtime
2+
3+
It is very common for a parallel program to
4+
spawn tasks at runtime.
5+
In Cpp-Taskflow, we call this *dynamic tasking* -
6+
creating another task dependency graph
7+
during the execution of a task.
8+
In this tutorial, we are going to demonstrate how to enable dynamic tasking
9+
in Cpp-Taskflow.
10+
11+
+ [Subflow Dependency Graph](#Subflow-Dependency-Graph)
12+
+ [Detach a Subflow Dependency Graph](#Detach-a-Subflow-Dependency-Graph)
13+
+ [Nested Subflow](#Nested-Subflow)
14+
15+
# Subflow Dependency Graph
16+
17+
Dynamic tasks are those created during the execution of a dispatched graph.
18+
These tasks are spawned from a parent task and are grouped together to a
19+
*subflow* dependency graph.
20+
Cpp-Taskflow has an unified interface for static and dynamic tasking.
21+
To create a subflow for dynamic tasking, emplace a callable
22+
that takes one argument of type `SubflowBuilder`.
23+
A `SubflowBuilder` object will be created during the runtime and
24+
passed to the task.
25+
All graph building methods you find in taskflow can be also used in a subflow builder.
26+
27+
```cpp
28+
1: tf::Taskflow tf(4); // create a taskflow object with four worker threads
29+
2:
30+
3: auto A = tf.silent_emplace([] () {}).name("A"); // static task A
31+
4: auto C = tf.silent_emplace([] () {}).name("C"); // static task C
32+
5: auto D = tf.silent_emplace([] () {}).name("D"); // static task D
33+
6:
34+
7: auto B = tf.silent_emplace([] (tf::SubflowBuilder& subflow) { // static task B to spawn a subflow
35+
8: auto B1 = subflow.silent_emplace([] () {}).name("B1"); // dynamic task B1
36+
9: auto B2 = subflow.silent_emplace([] () {}).name("B2"); // dynamic task B2
37+
10: auto B3 = subflow.silent_emplace([] () {}).name("B3"); // dynamic task B3
38+
11: B1.precede(B3); // B1 runs bofore B3
39+
12: B2.precede(B3); // B2 runs before B3
40+
13: }).name("B");
41+
14:
42+
15: A.precede(B); // B runs after A
43+
16: A.precede(C); // C runs after A
44+
17: B.precede(D); // D runs after B
45+
18: C.precede(D); // D runs after C
46+
19:
47+
20: tf.dispatch().get(); // execute the graph without cleanning up topologies
48+
21: std::cout << tf.dump_topologies();
49+
```
50+
51+
![](subflow_join.png)
52+
53+
Debrief:
54+
+ Line 1 creates a taskflow object with four worker threads
55+
+ Line 3-5 creates three tasks, A, C, and D
56+
+ Line 7-13 creates a task B that spawns a task dependency graph of three tasks B1, B2, and B3
57+
+ Line 15-18 add dependencies among A, B, C, and D
58+
+ Line 20 dispatches the graph and waits until it finishes without cleaning up the topology
59+
+ Line 21 dumps the topology that represents the entire task dependency graph
60+
61+
Line 7-13 is the main coding block to enable dynamic tasking.
62+
Cpp-Taskflow uses a variant date type to unify the interface of static tasking and dynamic tasking.
63+
The runtime will create a *subflow builder* passing it to task B,
64+
and spawn a dependency graph as described by the associated callable.
65+
This new subflow graph will be added to the topology to which its parent task B belongs to.
66+
Due to the property of dynamic tasking,
67+
we cannot dump its structure before execution.
68+
We will need to dispatch the graph first and call the method `dump_topologies`.
69+
70+
# Detach a Subflow Dependency Graph
71+
72+
By default, a spawned subflow joins its parent task.
73+
That is, all nodes of zero outgoing edges in the subflow will precede the parent task.
74+
This forces a subflow to follow the dependency constraints after its parent task.
75+
Having said that,
76+
you can detach a subflow from its parent task, allowing its execution to flow independently.
77+
78+
```cpp
79+
1: tf::Taskflow tf(4); // create a taskflow object with four worker threads
80+
2:
81+
3: auto A = tf.silent_emplace([] () {}).name("A"); // static task A
82+
4: auto C = tf.silent_emplace([] () {}).name("C"); // static task C
83+
5: auto D = tf.silent_emplace([] () {}).name("D"); // static task D
84+
6:
85+
7: auto B = tf.silent_emplace([] (tf::SubflowBuilder& subflow) { // task B to spawn a subflow
86+
8: auto B1 = subflow.silent_emplace([] () {}).name("B1"); // dynamic task B1
87+
9: auto B2 = subflow.silent_emplace([] () {}).name("B2"); // dynamic task B2
88+
10: auto B3 = subflow.silent_emplace([] () {}).name("B3"); // dynamic task B3
89+
11: B1.precede(B3); // B1 runs bofore B3
90+
12: B2.precede(B3); // B2 runs before B3
91+
13: subflow.detach(); // detach this subflow
92+
14: }).name("B");
93+
15:
94+
16: A.precede(B); // B runs after A
95+
17: A.precede(C); // C runs after A
96+
18: B.precede(D); // D runs after B
97+
19: C.precede(D); // D runs after C
98+
20:
99+
21: tf.dispatch().get(); // execute the graph without cleanning up topologies
100+
22: std::cout << tf.dump_topologies();
101+
```
102+
103+
![](subflow_detach.png)
104+
105+
The above figure demonstrates a detached subflow based on the example
106+
in the previous section.
107+
A detached subflow will eventually join the end of the topology of its parent task.
108+
109+
# Nested Subflow
110+
111+
A subflow can be nested or recursive.
112+
You can create another subflow from the execution of a subflow and so on.
113+
114+
```cpp
115+
1: tf::Taskflow tf;
116+
2:
117+
3: auto A = tf.silent_emplace([] (auto& sbf){
118+
4: std::cout << "A spawns A1 & subflow A2\n";
119+
5: auto A1 = sbf.silent_emplace([] () {
120+
6: std::cout << "subtask A1\n";
121+
7: }).name("A1");
122+
8:
123+
9: auto A2 = sbf.silent_emplace([] (auto& sbf2){
124+
10: std::cout << "A2 spawns A2_1 & A2_2\n";
125+
11: auto A2_1 = sbf2.silent_emplace([] () {
126+
12: std::cout << "subtask A2_1\n";
127+
13: }).name("A2_1");
128+
14: auto A2_2 = sbf2.silent_emplace([] () {
129+
15: std::cout << "subtask A2_2\n";
130+
16: }).name("A2_2");
131+
17: A2_1.precede(A2_2);
132+
18: }).name("A2");
133+
19: A1.precede(A2);
134+
20: }).name("A");
135+
21:
136+
22: // execute the graph without cleanning up topologies
137+
23: tf.dispatch().get();
138+
24: std::cout << tf.dump_topologies();
139+
```
140+
141+
![](nested_subflow.png)
142+
143+
Debrief:
144+
+ Line 1 creates a taskflow object
145+
+ Line 3-20 creates a task to spawn a subflow of two tasks A1 and A2
146+
+ Line 9-18 spawns another subflow of two tasks A2_1 and A2_2 out of its parent task A2
147+
+ Line 23-24 dispatches the graph asynchronously and dump its structure when it finishes
148+
149+
Similarly, you can detach a nested subflow from its parent subflow.
150+
A detached subflow will run independently and eventually join the topology
151+
of its parent subflow.
152+
153+
154+
155+

doc/examples/exec.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Execute a Task Dependency Graph
2+
3+
After you create a task dependency graph,
4+
you need to dispatch it for execution.
5+
In this tutorial, we will show you how to execute a
6+
task dependency graph.
7+
8+
+ [Graph and Topology](#Graph-and-Topology)
9+
+ [Blocking Execution](#Blocking-Execution)
10+
+ [Non-blocking Execution](#Non-blocking-Execution)
11+
+ [Wait on Topologies](#Wait-on-Topologies)
12+
+ [Example 1: Multiple Dispatches](#Example-1-Multiple-Dispatches)
13+
14+
# Graph and Topology
15+
16+
Each taskflow object has exactly one graph at a time to represent
17+
task dependencies constructed so far.
18+
The graph exists until users dispatch it for execution.
19+
We call a dispatched graph a *topology*.
20+
Each taskflow object has a list of topologies to keep track of the
21+
execution status of dispatched graphs.
22+
All tasks are executed in a shared thread pool.
23+
24+
# Blocking Execution
25+
26+
One way to dispatch the present task dependency graph
27+
is to use the method `wait_for_all`.
28+
Calling `wait_for_all` will dispatch the present graph to a shared thread storage
29+
and block the program until all tasks finish.
30+
31+
```cpp
32+
1: tf::Taskflow tf(4);
33+
2:
34+
3: auto A = tf.silent_emplace([] () { std::cout << "TaskA\n"; });
35+
4: auto B = tf.silent_emplace([] () { std::cout << "TaskB\n"; });
36+
5: A.precede(B);
37+
6:
38+
7: tf.wait_for_all();
39+
```
40+
41+
When `wait_for_all` returns, all tasks including previously dispatched ones
42+
are guaranteed to finish.
43+
All topologies will be cleaned up as well.
44+
45+
# Non-blocking Execution
46+
47+
Another way to dispatch the present task dependency graph
48+
is to use the method `dispatch` or `silent_dispatch`.
49+
These two methods will dispatch the present graph to threads
50+
and returns immediately without blocking the program.
51+
Non-blocking methods allows the program to perform other computations
52+
that can overlap with the execution of topologies.
53+
54+
```cpp
55+
1: tf::Taskflow tf(4);
56+
2:
57+
3: auto A = tf.silent_emplace([] () { std::cout << "TaskA\n"; });
58+
4: auto B = tf.silent_emplace([] () { std::cout << "TaskB\n"; });
59+
5: A.precede(B);
60+
6:
61+
7: auto F = tf.dispatch();
62+
8: // do some computation to overlap the execution of tasks A and B
63+
9: // ...
64+
10: F.get();
65+
```
66+
67+
Debrief:
68+
69+
+ Line 1-5 creates a graph with two tasks and one dependency
70+
+ Line 7 dispatches this graph to a topology
71+
and obtains a `std::future` to access its execution status
72+
+ Line 8-9 performs some computations to overlap the execution of this topology
73+
+ Line 10 blocks the program until this topology finishes
74+
75+
If you do not care the status of a dispatched graph,
76+
use the method `silent_dispatch`.
77+
This method does not return anything.
78+
79+
80+
```cpp
81+
1: tf::Taskflow tf(4);
82+
2:
83+
3: auto A = tf.silent_emplace([] () { std::cout << "TaskA\n"; });
84+
4: auto B = tf.silent_emplace([] () { std::cout << "TaskB\n"; });
85+
5: A.precede(B);
86+
6:
87+
7: auto F = tf.dispatch();
88+
8: // do some computation to overlap the execution of tasks A and B
89+
9: // ...
90+
```
91+
92+
# Wait on Topologies
93+
94+
When you call `dispatch` or `silent_dispatch`,
95+
the taskflow object will dispatch the present graph to threads
96+
and maintain a list of data structures called *topology*
97+
to store the execution status.
98+
These topologies are not cleaned up automatically on completion.
99+
100+
101+
```cpp
102+
1: tf::Taskflow tf(4);
103+
2:
104+
3: auto A = tf.silent_emplace([] () { std::cout << "TaskA\n"; });
105+
4: auto B = tf.silent_emplace([] () { std::cout << "TaskB\n"; });
106+
5: A.precede(B);
107+
6:
108+
7: auto F = tf.dispatch(); // dispatch the present graph
109+
8:
110+
9: auto C = tf.silent_emplace([] () { std::cout << "TaskC\n"; });
111+
10:
112+
11: tf.silent_dispatch(); // dispatch the present graph
113+
12:
114+
13: tf.wait_for_topologies(); // block until the two graphs finish
115+
14:
116+
15: assert(F.wait_for(std::chrono::seconds(0)) == std::future_status::ready);
117+
```
118+
119+
Debrief
120+
+ Line 1 creates a taskflow object with four worker threads
121+
+ Line 3-5 creates a dependency graph of two tasks and one dependency
122+
+ Line 7 dispatches the present graph to threads and obtains a future object
123+
to access the execution status
124+
+ Line 9 starts with a new dependency graph with one task
125+
+ Line 11 dispatches the present graph to threads
126+
+ Line 13 blocks the program until all running topologies finish
127+
128+
It's clear now Line 9 overlaps the execution of the first graph.
129+
After Line 11, there are two topologies running inside the taskflow object.
130+
Calling the method `wait_for_topologies` blocks the
131+
program until both complete.
132+
133+
# Example 1: Multiple Dispatches
134+
135+
The example below demonstrates how to create multiple task dependency graphs and dispatch each of them asynchronously.
136+
137+
```cpp
138+
1: #include <taskflow/taskflow.hpp>
139+
2:
140+
3: std::atomic<int> counter {0};
141+
4:
142+
5: void create_graph(tf::Taskflow& tf) {
143+
6: auto [A, B] = tf.silent_emplace(
144+
7: [&] () { counter.fetch_add(1, std::memory_order_relaxed); },
145+
8: [&] () { counter.fetch_add(1, std::memory_order_relaxed); }
146+
9: );
147+
10: }
148+
11:
149+
12: void multiple_dispatches() {
150+
13: tf::Taskflow tf(4);
151+
14: for(int i=0; i<10; ++i) {
152+
15: std::cout << "dispatch iteration " << i << std::endl;
153+
16: create_graph(tf);
154+
17: tf.silent_dispatch();
155+
18: }
156+
19: }
157+
20:
158+
21: int main() {
159+
22:
160+
23: multiple_dispatches();
161+
24: assert(counter == 20);
162+
25:
163+
26: return 0;
164+
27: }
165+
```
166+
167+
Debrief:
168+
+ Line 3 declares a global atomic variable initialized to zero
169+
+ Line 5-10 defines a function that takes a taskflow object and creates two tasks to increment the counter
170+
+ Line 12-19 defines a function that iteratively creates a task dependency graph and dispatches it asynchronously
171+
+ Line 23 starts the procedure of multiple dispatches
172+
173+
Notice in Line 24 the counter ends up with 20.
174+
The destructor of a taskflow object will not leave until all running
175+
topologies finish.
176+
177+
178+

doc/examples/nested_subflow.png

125 KB
Loading

0 commit comments

Comments
 (0)