Skip to content

Commit eefdb8c

Browse files
updated README
1 parent a7d3795 commit eefdb8c

13 files changed

+137
-109
lines changed

README.md

Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ visit the [documentation][wiki] to learn more about Cpp-Taskflow.
5353
Technical details can be referred to our [IEEE IPDPS19 paper][IPDPS19].
5454

5555
: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.
5858
Please refer to [release-notes](https://cpp-taskflow.github.io/cpp-taskflow/release-2-2-0.html)
5959
for adapting to this new change.
6060

@@ -154,28 +154,28 @@ b3.precede(T); // b3 runs before T
154154

155155
# Create a Taskflow Graph
156156

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.
158158
Most applications are developed through the following three steps.
159159

160160
## Step 1: Create a Task
161161

162-
Create a taskflow object to start with a task dependency graph:
162+
Create a taskflow object to build a task dependency graph:
163163

164164
```cpp
165-
tf::Taskflow tf;
165+
tf::Taskflow taskflow;
166166
```
167167

168168
A task is a callable object for which [std::invoke][std::invoke] is applicable.
169169
Use the method `emplace` to create a task:
170170

171171
```cpp
172-
tf::Task A = tf.emplace([](){ std::cout << "Task A\n"; });
172+
tf::Task A = taskflow.emplace([](){ std::cout << "Task A\n"; });
173173
```
174174
175-
You can create multiple tasks at one time.
175+
You can create multiple tasks at one time:
176176
177177
```cpp
178-
auto [A, B, C, D] = tf.emplace(
178+
auto [A, B, C, D] = taskflow.emplace(
179179
[] () { std::cout << "Task A\n"; },
180180
[] () { std::cout << "Task B\n"; },
181181
[] () { std::cout << "Task C\n"; },
@@ -185,68 +185,72 @@ auto [A, B, C, D] = tf.emplace(
185185

186186
## Step 2: Define Task Dependencies
187187

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).
190191
The handle `Task` supports different methods for you to describe task dependencies.
191192

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.
193194
```cpp
194195
A.precede(B); // A runs before B.
195196
```
196197

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.
198199
```cpp
199200
A.gather(B); // A runs after B
200201
```
201202

202203
## Step 3: Execute a Taskflow
203204

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.
207208

208209
```cpp
209-
tf::Executor executor; // executor manages a set of worker threads
210+
tf::Executor executor;
210211
```
211212

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]
215216
to let you query the execution status.
216217

217218
```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
222223

223224
// 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; } );
225226

226227
// 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"; } );
229230
```
230231
231232
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.
233234
234235
```cpp
235236
executor.wait_for_all(); // block until all associated tasks finish
236237
```
237238

238239
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.
240244

241245
# Dynamic Tasking
242246

243247
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.
245249
These tasks are spawned by a parent task and are grouped together to a *subflow* graph.
246250
The example below demonstrates how to create a subflow
247251
that spawns three tasks at runtime.
248252

249-
<img align="right" src="image/subflow_join.png" width="35%">
253+
<img align="right" src="image/subflow_join.png" width="30%">
250254

251255
```cpp
252256
// create three regular tasks
@@ -273,7 +277,7 @@ By default, a subflow graph joins to its parent node.
273277
This guarantees a subflow graph to finish before the successors of
274278
its parent node.
275279
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:
277281

278282
<img align="right" src="image/subflow_detach.png" width="35%">
279283

@@ -295,16 +299,16 @@ tf::Task B = tf.emplace([] (tf::SubflowBuilder& subflow) {
295299

296300
Cpp-Taskflow has an unified interface for static and dynamic tasking.
297301
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`.
299303

300304
```cpp
301305
tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) {});
302306
```
303307

304308
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.
306310
All graph building methods defined in taskflow
307-
can be used in a subflow builder.
311+
can be used in the subflow builder.
308312

309313
```cpp
310314
tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) {
@@ -317,7 +321,7 @@ tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) {
317321
});
318322
```
319323

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
321325
the execution of a subflow and so on.
322326

323327
<img align="right" src="image/nested_subflow.png" width="25%">
@@ -356,8 +360,8 @@ tf::Task A = tf.emplace([] (tf::SubflowBuilder& subflow) {
356360
}); // subflow starts to run after the callable scope
357361
```
358362

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.
361365
In a joined subflow,
362366
the completion of its parent node is defined as when all tasks
363367
inside the subflow (possibly nested) finish.
@@ -439,8 +443,8 @@ f2B.precede(f1_module_task);
439443
f1_module_task.precede(f2C);
440444
```
441445
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.
444448
You can compose a taskflow from multiple taskflows and use the result
445449
to compose another taskflow and so on.
446450
@@ -463,19 +467,19 @@ You can dump it to a GraphViz format using the method `dump`.
463467
464468
```cpp
465469
// debug.cpp
466-
tf::Taskflow tf;
470+
tf::Taskflow taskflow;
467471
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");
473477
474478
A.precede(B, C, E);
475479
C.precede(D);
476480
B.precede(D, E);
477481
478-
tf.dump(std::cout);
482+
taskflow.dump(std::cout);
479483
```
480484

481485
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.
561565
562566
# API Reference
563567
564-
The official [documentation][wiki] explains the complete list of
568+
The official [documentation][wiki] explains a complete list of
565569
Cpp-Taskflow API.
566570
In this section, we highlight a short list of commonly used methods.
567571
@@ -583,7 +587,7 @@ The table below summarizes a list of commonly used methods.
583587
584588
### *emplace/placeholder*
585589
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.
587591
588592
```cpp
589593
// create a task through emplace
@@ -644,7 +648,7 @@ auto [S, T] = tf.parallel_for(
644648
By default, taskflow performs an even partition over worker threads
645649
if the group size is not specified (or equal to 0).
646650
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.
648652
The first three argument to this overload indicates
649653
starting index, ending index (exclusive), and step size.
650654
@@ -678,7 +682,7 @@ auto [S, T] = tf.parallel_for(
678682
679683
The method `reduce` creates a subgraph that applies a binary operator to a range of items.
680684
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.
682686
683687
<img align="right" width="45%" src="image/reduce.png">
684688
@@ -702,15 +706,15 @@ auto [S, T] = tf.transform_reduce(v.begin(), v.end(), min,
702706
);
703707
```
704708
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`.
706710
707711
## Task API
708712
709713
Each time you create a task, the taskflow object adds a node to the present task dependency graph
710714
and return a *task handle* to you.
711715
A task handle is a lightweight object that defines a set of methods for users to
712716
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.
714718
715719
| Method | Argument | Return | Description |
716720
| -------------- | ----------- | ------ | ----------- |
@@ -739,7 +743,7 @@ A.work([] () { std::cout << "hello world!"; });
739743
740744
### *precede*
741745
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.
743747
744748
<img align="right" width="20%" src="image/precede.png">
745749
@@ -760,7 +764,7 @@ A.precede(B, C, D, E);
760764

761765
### *gather*
762766

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.
764768

765769
<img align="right" width="30%" src="image/gather.png">
766770

@@ -785,33 +789,38 @@ The table below summarizes a list of commonly used methods.
785789

786790
### *Executor*
787791

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.
789794

790795
```cpp
791796
tf::Executor executor(8); // create an executor of 8 worker threads
792797
```
793798
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.
795801
796802
### *run/run_n/run_until*
797803
798804
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.
800807
801808
```cpp
802809
executor.run(taskflow); // run a graph once
803810
executor.run_n(taskflow, 5); // run a graph five times
804811
executor.run_n(taskflow, my_pred); // keep running a graph until the predicate becomes true
805812
```
806813

814+
The first run finishes before the second run, and the second run finishes before the third run.
815+
807816
# Caveats
808817

809818
While Cpp-Taskflow enables the expression of very complex task dependency graph that might contain
810819
thousands of task nodes and links, there are a few amateur pitfalls and mistakes to be aware of.
811820

812821
+ 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
813823
+ Trying to modify a running task can result in undefined behavior
814-
+ Touching a taskflow from multiple threads are not safe
815824

816825
Cpp-Taskflow is known to work on Linux distributions, MAC OSX, and Microsoft Visual Studio.
817826
Please [let me know][email me] if you found any issues in a particular platform.

0 commit comments

Comments
 (0)