• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..03-May-2022-

docs/H03-May-2022-2,0461,707

examples/H03-May-2022-806598

include/H12-Apr-2021-3,7252,688

test/H12-Apr-2021-15,73812,337

.gitignoreH A D12-Apr-2021177 1817

.travis.ymlH A D12-Apr-2021907 3834

CHANGES.mdH A D12-Apr-20214.1 KiB13896

LICENSE.mdH A D12-Apr-20211.1 KiB2217

README.mdH A D12-Apr-202115.4 KiB410335

appveyor.ymlH A D12-Apr-2021815 4030

check.shH A D12-Apr-2021990 4639

conanfile.pyH A D12-Apr-2021725 2419

doxyfileH A D12-Apr-2021585 3127

valgrind.shH A D12-Apr-2021333 1412

README.md

1# transwarp
2
3[![Gitter](https://badges.gitter.im/bloomen/transwarp.svg)](https://gitter.im/bloomen/transwarp) [![Travis](https://travis-ci.org/bloomen/transwarp.svg?branch=master)](https://travis-ci.org/bloomen/transwarp/branches) [![Appveyor](https://ci.appveyor.com/api/projects/status/wrtbk9l3b94eeb9t/branch/master?svg=true)](https://ci.appveyor.com/project/bloomen/transwarp?branch=master)
4
5<a href="https://bloomen.github.io/transwarp">Doxygen documentation</a>
6
7transwarp is a header-only C++ library for task concurrency. It
8allows you to easily create a graph of tasks where every task can be executed
9asynchronously. transwarp is written in C++17 and only depends on the standard
10library. Just copy `src/transwarp.h` to your project and off you go!
11Tested with GCC, Clang, ICC, and Visual Studio.
12
13C++11 support can be enabled by defining `TRANSWARP_CPP11` at compile time.
14
15**Important:** Only use tagged releases of transwarp in production code!
16
17**Table of contents**
18
19  * [Example](#example)
20  * [API doc](#api-doc)
21     * [Creating tasks](#creating-tasks)
22     * [Scheduling tasks](#scheduling-tasks)
23     * [Executors](#executors)
24     * [Range functions](#range-functions)
25     * [Canceling tasks](#canceling-tasks)
26     * [Event system](#event-system)
27     * [Task pool](#task-pool)
28     * [Timing tasks](#timing-tasks)
29     * [Optimizing efficiency](#optimizing-efficiency)
30  * [Feedback](#feedback)
31  * [Contributors](#contributors)
32
33## Example
34
35This example creates three tasks and connects them with each other to form
36a two-level graph. The tasks are then scheduled twice for computation
37while using 4 threads.
38```cpp
39#include <fstream>
40#include <iostream>
41#include "transwarp.h"
42
43namespace tw = transwarp;
44
45int main() {
46    double x = 0;
47    int y = 0;
48
49    // Building the task graph
50    auto parent1 = tw::make_task(tw::root, [&x]{ return 13.3 + x; })->named("something");
51    auto parent2 = tw::make_task(tw::root, [&y]{ return 42 + y; })->named("something else");
52    auto child = tw::make_task(tw::consume, [](double a, int b) { return a + b;
53                                            }, parent1, parent2)->named("adder");
54
55    tw::parallel executor{4};  // Parallel execution with 4 threads
56
57    child->schedule_all(executor);  // Schedules all tasks for execution
58    std::cout << "result = " << child->get() << std::endl;  // result = 55.3
59
60    // Modifying data input
61    x += 2.5;
62    y += 1;
63
64    child->schedule_all(executor);  // Re-schedules all tasks for execution
65    std::cout << "result = " << child->get() << std::endl;  // result = 58.8
66
67    // Creating a dot-style graph for visualization
68    std::ofstream{"basic_with_three_tasks.dot"} << tw::to_string(child->edges());
69}
70```
71
72The resulting graph of this example looks like this:
73
74![graph](https://raw.githubusercontent.com/bloomen/transwarp/master/examples/basic_with_three_tasks.png)
75
76Every bubble represents a task and every arrow an edge between two tasks.
77The first line within a bubble is the task name. The second line denotes the task
78type followed by the task id and the task level in the graph.
79
80## API doc
81
82This is a brief API doc of transwarp.
83For more details check out the <a href="https://bloomen.github.io/transwarp">doxygen documentation</a>
84and the <a href="https://github.com/bloomen/transwarp/tree/master/examples">transwarp examples</a>.
85
86In the following we will use `tw` as a namespace alias for `transwarp`.
87
88### Creating tasks
89
90transwarp supports seven different task types:
91```cpp
92root, // The task has no parents
93accept, // The task's functor accepts all parent futures
94accept_any, // The task's functor accepts the first parent future that becomes ready
95consume, // The task's functor consumes all parent results
96consume_any, // The task's functor consumes the first parent result that becomes ready
97wait, // The task's functor takes no arguments but waits for all parents to finish
98wait_any, // The task's functor takes no arguments but waits for the first parent to finish
99```
100The task type is passed as the first parameter to `make_task`, e.g., to create
101a `consume` task simply do this:
102```cpp
103auto task = tw::make_task(tw::consume, functor, parent1, parent2);
104```
105where `functor` denotes some callable and `parent1/2` the parent tasks.
106
107The functor as passed to `make_task` needs to fulfill certain requirements based
108on the task type and the given parents:
109
110**_root_**: A task at the root (top) of the graph. This task gets executed first.
111A functor to a `root` task cannot have any parameters since this task does not
112have parent tasks, e.g.:
113```cpp
114auto task = tw::make_task(tw::root, []{ return 42; });
115```
116Another way of defining a`root` task is a _value task_ which can be created as:
117```cpp
118auto task = tw::make_value_task(42);
119```
120A value task doesn't require scheduling and always returns the same value or exception.
121
122**_accept_**: This task is required to have at least one parent. It _accepts_
123the resulting parent futures as they are without unwrapping. Hence, the child
124can decide how to proceed since a call to `get()` can potentially throw an
125exception. Here's an example:
126```cpp
127auto task = tw::make_task(tw::accept, [](auto f1, auto f2) { return f1.get() + f2.get(); }, parent1, parent2);
128```
129
130**_accept_any_**: This task is required to have at least one parent but its
131functor takes exactly one future, namely the future of the parent that
132first finishes. All other parents are abandoned and canceled. Here's an example:
133```cpp
134auto task = tw::make_task(tw::accept_any, [](auto f1) { return f1.get(); }, parent1, parent2);
135```
136Note that canceling only works for already running tasks when the functor is
137sub-classed from `transwarp::functor`.
138
139**_consume_**: This task follows the same rules as `accept` with the difference
140that the resulting parent futures are unwrapped (have `get()` called on them).
141The results are then passed to the child, hence, consumed by the child task.
142The child task will not be invoked if any parent throws an exception.
143For example:
144```cpp
145auto task = tw::make_task(tw::consume, [](int x, int y) { return x + y; }, parent1, parent2);
146```
147
148**_consume_any_**: This task follows the same rules as `accept_any` with the difference
149that the resulting parent futures are unwrapped (have `get()` called on them).
150For example:
151```cpp
152auto task = tw::make_task(tw::consume_any, [](int x) { return x; }, parent1, parent2);
153```
154
155**_wait_**: This task's functor does not take any parameters but the task
156must have at least one parent. It simply waits for completion of all parents
157while unwrapping futures before calling the child's functor. For example:
158```cpp
159auto task = tw::make_task(tw::wait, []{ return 42; }, parent1, parent2);
160```
161
162**_wait_any_**: This task works similar to the `wait` task but calls its functor
163as soon as the first parent completes. It abandons and cancels all remaining
164parent tasks. For example:
165```cpp
166auto task = tw::make_task(tw::wait_any, []{ return 42; }, parent1, parent2);
167```
168
169Generally, tasks are created using `make_task` which allows for any number
170of parents. However, it is a common use case for a child to only have one parent.
171For this, `next()` can be directly called on the parent object to create a _continuation_:
172```cpp
173auto child = tw::make_task(tw::root, []{ return 42; })->next(tw::consume, functor);
174```
175`child` is now a single-parent task whose functor consumes an integer.
176
177### Scheduling tasks
178
179Once a task is created it can be scheduled just by itself:
180```cpp
181auto task = tw::make_task(tw::root, functor);
182task->schedule();
183```
184which, if nothing else is specified, will run the task on the current thread.
185However, using the built-in `parallel` executor the task can be pushed into a
186thread pool and executed asynchronously:
187```cpp
188tw::parallel executor{4};  // Thread pool with 4 threads
189auto task = tw::make_task(tw::root, functor);
190task->schedule(executor);
191```
192Regardless of how you schedule, the task result can be retrieved through:
193```cpp
194std::cout << task->get() << std::endl;
195```
196When chaining multiple tasks together a directed acyclic graph is built in which
197every task can be scheduled individually. Though, in many scenarios it is useful
198to compute all tasks in the right order with a single call:
199```cpp
200auto parent1 = tw::make_task(tw::root, foo);  // foo is a functor
201auto parent2 = tw::make_task(tw::root, bar);  // bar is a functor
202auto task = tw::make_task(tw::consume, functor, parent1, parent2);
203task->schedule_all();  // Schedules all parents and itself
204```
205which can also be scheduled using an executor, for instance:
206```cpp
207tw::parallel executor{4};
208task->schedule_all(executor);
209```
210which will run those tasks in parallel that do not depend on each other.
211
212### Executors
213
214We have seen that we can pass executors to `schedule()` and `schedule_all()`.
215Additionally, they can be assigned to a task directly:
216```cpp
217auto exec1 = std::make_shared<tw::parallel>(2);
218task->set_executor(exec1);
219tw::sequential exec2;
220task->schedule(exec2);  // exec1 will be used to schedule the task
221```
222The task-specific executor will always be preferred over other executors when
223scheduling tasks.
224
225transwarp defines an executor interface which can be implemented to perform custom
226behavior when scheduling tasks. The interface looks like this:
227```cpp
228class executor {
229public:
230    virtual ~executor() = default;
231
232    // The name of the executor
233    virtual std::string name() const = 0;
234
235    // Only ever called on the thread of the caller to schedule()
236    virtual void execute(const std::function<void()>& functor, tw::itask& task) = 0;
237};
238
239```
240where `functor` denotes the function to be run and `task` the task the functor belongs to.
241
242### Range functions
243
244There are convenience functions that can be applied to an iterator range:
245* `tw::for_each`
246* `tw::transform`
247
248These are very similar to their standard library counterparts except that they
249return a task for deferred, possibly asynchronous execution. Here's an example:
250
251```cpp
252std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7};
253tw::parallel exec{4};
254auto task = tw::for_each(exec, vec.begin(), vec.end(), [](int& x){ x *= 2; });
255task->wait();  // all values in vec will have doubled
256```
257
258### Canceling tasks
259
260A task can be canceled by calling `task->cancel(true)` which will, by default,
261only affect tasks that are not currently running yet. However, if you create a functor
262that inherits from `transwarp::functor` you can terminate tasks while they're
263running. `transwarp::functor` looks like this:
264```cpp
265
266class functor {
267public:
268    virtual ~functor() = default;
269
270protected:
271    // The associated task (only to be called after the task was constructed)
272    const tw::itask& transwarp_task() const noexcept;
273
274    // The associated task (only to be called after the task was constructed)
275    tw::itask& transwarp_task() noexcept;
276
277    // If the associated task is canceled then this will throw transwarp::task_canceled
278    // which will stop the task while it's running (only to be called after the task was constructed)
279    void transwarp_cancel_point() const;
280
281private:
282    ...
283};
284```
285By placing calls to `transwarp_cancel_point()` in strategic places of your functor
286you can denote well defined points where the functor will exit when the associated task is canceled.
287A task can also be canceled by throwing `transwarp::task_canceled` directly.
288
289As mentioned above, tasks can be explicitly canceled on client request. In addition,
290all tasks considered abandoned by `accept_any`, `consume_any`, or `wait_any`
291operations are also canceled in order to terminate them as soon as their computations
292become superfluous.
293
294### Event system
295
296Transwarp provides an event system that allows you to subscribe to all or specific
297events of a task, such as, before started or after finished events. The task events
298are enumerated in the `event_type` enum:
299```cpp
300enum class event_type {
301    before_scheduled, // Just before a task is scheduled
302    after_future_changed, // Just after the task's future was changed
303    before_started, // Just before a task starts running
304    before_invoked, // Just before a task's functor is invoked
305    after_finished, // Just after a task has finished running
306    after_canceled, // Just after a task was canceled
307    after_satisfied, ///< Just after a task has satisfied all its children with results
308    after_custom_data_set, // Just after custom data was assigned
309}
310```
311Listeners are created by sub-classing from the `listener` interface:
312```cpp
313class listener {
314public:
315    virtual ~listener() = default;
316
317    // This may be called from arbitrary threads depending on the event type
318    virtual void handle_event(tw::event_type event, tw::itask& task) = 0;
319};
320```
321A listener can then be passed to the `add_listener` functions of a task
322to add a new listener or to the `remove_listener` functions to remove
323an existing listener.
324
325### Task pool
326
327A task pool is useful when one wants to run the same graph in parallel. For this purpose,
328transwarp provides a `task_pool` which manages a pool of tasks from which
329one can request an idle task for parallel graph execution. For example:
330```cpp
331tw::parallel exec{4};
332
333auto my_task = make_graph();
334tw::task_pool<double> pool{my_task};
335
336for (;;) {
337    auto task = pool.next_task(); // task may be null if the pool size is exhausted
338    if (task) {
339        task->schedule_all(exec);
340    }
341}
342```
343
344### Timing tasks
345
346In order to identify bottlenecks it's often useful to know how much time is spent
347in which task. transwarp provides a `timer` listener that will automatically
348time the tasks it listens to:
349```cpp
350auto task = make_graph();
351task->add_listener_all(std::make_shared<tw::timer>()); // assigns the timer listener to all tasks
352task->schedule_all();
353std::ofstream{"graph.dot"} << tw::to_string(task->edges()); // the dot file now contains timing info
354```
355
356### Optimizing efficiency
357
358**Compile time switches**
359
360By default, transwarp provides its full functionality to its client. However,
361in many cases not all of that is actually required and so transwarp provides
362a few compile time switches to reduce the task size.
363These switches are:
364```
365TRANSWARP_DISABLE_TASK_CUSTOM_DATA
366TRANSWARP_DISABLE_TASK_NAME
367TRANSWARP_DISABLE_TASK_PRIORITY
368TRANSWARP_DISABLE_TASK_REFCOUNT
369TRANSWARP_DISABLE_TASK_TIME
370```
371
372To get the minimal task size with a single switch one can define
373```
374TRANSWARP_MINIMUM_TASK_SIZE
375```
376at build time.
377
378**Releasing unused memory**
379
380By default, every task in a graph will keep its result until rescheduling or
381a manual task reset. The `releaser` listener allows you to automatically
382release a task result after that task's children have consumed the result.
383For example:
384```cpp
385auto task = make_graph();
386task->add_listener_all(std::make_shared<tw::releaser>()); // assigns the releaser listener to all tasks
387task->schedule_all();
388// All intermediate task results are now released (i.e. futures are invalid)
389auto result = task->get(); // The final task's result remains valid
390```
391The `releaser` also accepts an executor that gives control over _where_ a task's
392result is released.
393
394## Feedback
395
396Get in touch if you have any questions or suggestions to make this a better library!
397You can post on [gitter](https://gitter.im/bloomen/transwarp), submit a pull request,
398create a Github issue, or simply email one of the contributors.
399
400If you're serious about contributing code to transwarp (which would be awesome!) then
401please submit a pull request and keep in mind that:
402- unit tests should be added for all new code by extending the existing unit test suite
403- C++ code uses spaces throughout
404
405## Contributors
406
407- @[bloomen](https://github.com/bloomen)
408- @[guancodes](https://github.com/guancodes)
409- @[acdemiralp](https://github.com/acdemiralp)
410