Qt TaskTree C++ Classes | Qt TaskTree | Qt 6.12.0
Description: Contains a general purpose TaskTree library.
Keywords: module

- Qt 6.12
- Qt TaskTree
- Qt TaskTree C++ Classes
- Qt 6.12.0 Reference Documentation

# Qt TaskTree C++ Classes

Contains a general purpose TaskTree library. More...

This module is in technology preview and is subject to change.

This module was introduced in Qt 6.11.

## Namespaces

QtTaskTree | Encloses all classes and global functions of the TaskTree module

## Classes

QtTaskTree::Do | A body element used with For and When constructs
QtTaskTree::Else | An "else" element used in conditional expressions
QtTaskTree::ElseIf | An "else if" element used in conditional expressions
QtTaskTree::ExecutableItem | Base class for executable task items
QtTaskTree::ExecutionMode | Group element describing execution mode
QtTaskTree::For | A for loop element
QtTaskTree::Forever | Infinite loop of subtasks
QtTaskTree::ForeverIterator | Infinite iterator to be used inside For element
QtTaskTree::Group | Represents the basic element for composing declarative recipes describing how to execute and handle a nested tree of asynchronous tasks
QtTaskTree::GroupItem | Represents the basic element that may be a part of any Group
QtTaskTree::If | An "if" element used in conditional expressions
QtTaskTree::Iterator | Base class to be used as an iterator inside For element
QtTaskTree::ListIterator | List iterator to be used inside For element
QtTaskTree::ObjectSignal | Structure describing QObject subclass and its signal
QtTaskTree::ParallelLimit | Parallel execution mode with a custom limit
QtTaskTree::QBarrier | An asynchronous task that finishes on demand
QtTaskTree::QCustomTask | A class template used for declaring custom task items and defining their setup and done handlers
QtTaskTree::QDefaultTaskAdapter | A class template providing default task adapter used in QCustomTask
QtTaskTree::QMappedTaskTreeRunner | A mapped task tree execution controller with a given Key type
QtTaskTree::QNetworkReplyWrapper | A wrapper around QNetworkReply and QNetworkAccessManager
QtTaskTree::QParallelTaskTreeRunner | A parallel task tree execution controller
QtTaskTree::QProcessTaskDeleter | A custom deleter for QProcess, used by QProcessTask
QtTaskTree::QSequentialTaskTreeRunner | A sequential task tree execution controller
QtTaskTree::QSingleTaskTreeRunner | A single task tree execution controller
QtTaskTree::QStartedBarrier | A started QBarrier with a given limit
QtTaskTree::QSyncTask | Synchronously executes a custom handler between other tasks
QtTaskTree::QTaskInterface | Helper class used when adapting custom task's interface
QtTaskTree::QTaskTree | Runs the tree of asynchronous tasks defined in a declarative way
QtTaskTree::QTcpSocketWrapper | A wrapper around QTcpSocket
QtTaskTree::QThreadFunction | A class template controlling the execution of a function in a separate thread via QtConcurrent::run()
QtTaskTree::QThreadFunctionBase | A base class for QThreadFunction class template
QtTaskTree::RepeatIterator | Repetitive iterator to be used inside For element
QtTaskTree::Storage | A class template for custom data exchange in the running task tree
QtTaskTree::Then | A "then" element used in conditional expressions
QtTaskTree::UntilIterator | Conditional iterator to be used inside For element
QtTaskTree::When | An element delaying the execution of a body until barrier advance

## Detailed Description

Use the TaskTree library to construct recipes, describing what asynchronous tasks are to be executed, and use these recipes within QTaskTree to execute them.

The recipes are declarative descriptions on what task types are to be created and executed, e.g.: QProcess, QNetworkReplyWrapper, or QThreadFunction<`ReturnType`>, or whether they should run in sequence or in parallel. Inside recipes you may define different continuation paths depending on whether the previous task finished with success or an error. It's also possible to nest tasks in Group elements, and each Group may run its tasks according to it's own execution mode or workflow policy. The recipes form the task tree structures.

### Asynchronous Tasks

Asynchronous task is any task that can be started, and later it finishes with success or an error. Later means that after starting the task, the control goes back to the running event loop. We don’t block the caller thread until the task is finished. In order to use the task tree we need an event loop spinning.

Examples of asynchronous Tasks:

- QTimer::singleShot()
- QProcess
- QNetworkAccessManager + QNetworkReply = QNetworkReplyWrapper
- QtConcurrent::run() + QFutureWatcher<Result> = QThreadFunction<Result>

### Recipe & Task Tree

In order to memorize what the recipe and task tree is, let's use the analogy to the cartridge and player. When we write a recipe, it's like we would be constructing a cartridge, so we just prepare a detailed description on what the player should do later, when the cartridge is placed inside a player (task tree) and started. The recipe itself is just a declarative description for the task tree on what the task tree should do when the recipe is passed to the task tree and the task tree is started. The recipe itself doesn't do anything on its own without a task tree - it's like a cartridge without a player.

Here is a short summary about recipe and task tree responsibilities.

Recipe (cartridge) describes:

- what tasks are to be created dynamically by the running task tree (via QCustomTask)
- in which order
- what data structures are to be created dynamically by the running task tree (via Storage)
- how to setup each task before start
- how to collect data when tasks are finished
- execution mode (tasks should run in sequence or in parallel)
- workflow policy

Task tree (player):

- reads the recipe and creates tasks and data structures automatically
- manages the lifetime of created tasks and data structures
- executes continuations
- chooses different paths depending on results of finished tasks and workflow policies
- provides basic progress info

### Custom Tasks

Since the recipe is a description for the task tree on what tasks it should create when the task tree is started, we can't create these tasks directly inside a recipe. Instead, we need a declarative way to tell the task tree to create and start these tasks for us at later point in time. For example, if we want the task tree to create and start QProcess, we describe it by placing QProcessTask element inside a recipe. The QProcessTask is an alias to the QCustomTask<QProcess>. Each task Type should provide its corresponding QCustomTask<`Type`> so that it may be used inside recipes.

The following table shows some build-in custom tasks ready to be placed inside recipes:

Custom Task (used in recipes) | Task Class (created by the running task tree) | Brief Description
--- | --- | ---
QProcessTask | QProcess | Starts process.
QThreadFunctionTask<`ReturnType`> | QThreadFunction<`ReturnType`> | Starts asynchronous task, runs in separate thread.
QTaskTreeTask | QTaskTree | Starts nested task tree.
QNetworkReplyWrapperTask | QNetworkReplyWrapper | Starts network download.
QTcpSocketWrapperTask | QTcpSocketWrapper | Starts a TCP connection.

See QTaskInterface and Task Adapters for more information on how to adapt particular task to be used inside recipes.

### Example Recipe

The QTaskTree has a top level Group element, a.k.a recipe, which may contain any number of tasks of various types, such as QProcessTask, QNetworkReplyWrapperTask, or QThreadFunctionTask<`ReturnType`>:

     const Group recipe {
         QProcessTask(...),
         QNetworkReplyWrapperTask(...),
         QThreadFunctionTask<int>(...)
     };

     QTaskTree *taskTree = new QTaskTree(recipe);
     connect(taskTree, &QTaskTree::done, ...);  // finish handler
     taskTree->start();

The recipe above consist of a top level element of the Group type that contains tasks of the QProcessTask, QNetworkReplyWrapperTask, and QThreadFunctionTask<`int`> type. After `taskTree->start()` is called, the tasks are created and run in a chain, starting with QProcess. When the QProcess finishes successfully, the QNetworkReplyWrapper task is started. Finally, when the network task finishes successfully, the QThreadFunction<`int`> task is started.

When the last running task finishes with success, the task tree is considered to have run successfully and the QTaskTree::done() signal is emitted with DoneWith::Success. When a task finishes with an error, the execution of the task tree is stopped and the remaining tasks are skipped. The task tree finishes with an error and sends the QTaskTree::done() signal with DoneWith::Error.

### Groups

The parent of the Group sees it as a single task. Like other tasks, the group can be started and it can finish with success or an error. The Group elements can be nested to create a tree structure:

     const Group recipe {
         Group {
             parallel,
             QProcessTask(...),
             QThreadFunctionTask<int>(...)
         },
         QNetworkReplyWrapperTask(...)
     };

The example above differs from the first example in that the top level element has a subgroup that contains the QProcessTask and QThreadFunctionTask<`int`>. The subgroup is a sibling element of the QNetworkReplyWrapperTask in the root. The subgroup contains an additional parallel element that instructs its Group to execute its tasks in parallel.

So, when the QTaskTree starts the recipe above, the QProcess and QThreadFunction<`int`> start immediately and run in parallel. Since the root group doesn't contain a parallel element, its direct child tasks are run in sequence. Thus, the QNetworkReplyWrapper starts when the whole subgroup finishes. The group is considered as finished when all its tasks have finished. The order in which the tasks finish is not relevant.

So, depending on which task lasts longer (QProcess or QThreadFunction<`int`>), the following scenarios can take place:

Scenario 1 | Scenario 2
--- | ---
Root Group starts | Root Group starts
Sub Group starts | Sub Group starts
QProcess starts | QProcess starts
QThreadFunction<int> starts | QThreadFunction<int> starts
... | ...
QProcess finishes | QThreadFunction<int> finishes
... | ...
QThreadFunction<int> finishes | QProcess finishes
Sub Group finishes | Sub Group finishes
QNetworkReplyWrapper starts | QNetworkReplyWrapper starts
... | ...
QNetworkReplyWrapper finishes | QNetworkReplyWrapper finishes
Root Group finishes | Root Group finishes

The differences between the scenarios are marked with bold. Three dots mean that an unspecified amount of time passes between previous and next events (a task or tasks continue to run). No dots between events means that they occur synchronously.

The presented scenarios assume that all tasks run successfully. If a task fails during execution, the task tree finishes with an error. In particular, when QProcess finishes with an error while QThreadFunction<`int`> is still being executed, the QThreadFunction<`int`> is automatically canceled, the subgroup finishes with an error, the QNetworkReplyWrapper is skipped, and the tree finishes with an error.

### Task Handlers

Use `Task` handlers to set up a task for execution and to enable reading the output data from the task when it finishes with success or an error.

#### Task's Start Handler

When a task object is created and before it's started, the task tree invokes an optionally user-provided setup handler. The setup handler should always take a reference to the associated task class object:

     const auto onSetup = [](QProcess &process) {
         process.setProgram("sleep");
         process.setArguments({"3"});
     };
     const Group root {
         QProcessTask(onSetup)
     };

You can modify the passed QProcess in the setup handler, so that the task tree can start the process according to your configuration. You should not call `process.start();` in the setup handler, as the task tree calls it when needed. The setup handler is optional. When used, it must be the first argument of the task's constructor.

Optionally, the setup handler may return a SetupResult. The returned SetupResult influences the further start behavior of a given task. The possible values are:

SetupResult Value | Brief Description
--- | ---
Continue | The task will be started normally. This is the default behavior when the setup handler doesn't return SetupResult (that is, its return type is `void`).
StopWithSuccess | The task won't be started and it will report success to its parent.
StopWithError | The task won't be started and it will report an error to its parent.

This is useful for running a task only when a condition is met and the data needed to evaluate this condition is not known until previously started tasks finish. In this way, the setup handler dynamically decides whether to start the corresponding task normally or skip it and report success or an error. For more information about inter-task data exchange, see Storage.

#### Task's Done Handler

When a running task finishes, the task tree invokes an optionally provided done handler. The handler should take a `const` reference to the associated task class object:

     const auto onSetup = [](QProcess &process) {
         process.setProgram("sleep");
         process.setArguments({"3"});
     };
     const auto onDone = [](const QProcess &process, DoneWith result) {
         if (result == DoneWith::Success)
             qDebug() << "Success" << process.cleanedStdOut();
         else
             qDebug() << "Failure" << process.cleanedStdErr();
     };
     const Group root {
         QProcessTask(onSetup, onDone)
     };

The done handler may collect output data from QProcess, and store it for further processing or perform additional actions.

Note: If the task setup handler returns StopWithSuccess or StopWithError, the done handler is not invoked.

### Group Handlers

Similarly to task handlers, group handlers enable you to set up a group to execute and to apply more actions when the whole group finishes with success or an error.

#### Group's Start Handler

The task tree invokes the group start handler before it starts the child tasks. The group handler doesn't take any arguments:

     const auto onSetup = [] {
         qDebug() << "Entering the group";
     };
     const Group root {
         onGroupSetup(onSetup),
         QProcessTask(...)
     };

The group setup handler is optional. To define a group setup handler, add an onGroupSetup() element to a group. The argument of onGroupSetup() is a user handler. If you add more than one onGroupSetup() element to a group, an assert is triggered at runtime that includes an error message.

Like the task's start handler, the group start handler may return SetupResult. The returned SetupResult value affects the start behavior of the whole group. If you do not specify a group start handler, or its return type is void, the default group's action is Continue, so that all tasks are started normally. Otherwise, when the start handler returns StopWithSuccess or StopWithError, the tasks are not started (they are skipped) and the group itself reports success or an error, depending on the returned value, respectively.

     const Group root {
         onGroupSetup([] { qDebug() << "Root setup"; }),
         Group {
             onGroupSetup([] { qDebug() << "Group 1 setup"; return SetupResult::Continue; }),
             QProcessTask(...) // Process 1
         },
         Group {
             onGroupSetup([] { qDebug() << "Group 2 setup"; return SetupResult::StopWithSuccess; }),
             QProcessTask(...) // Process 2
         },
         Group {
             onGroupSetup([] { qDebug() << "Group 3 setup"; return SetupResult::StopWithError; }),
             QProcessTask(...) // Process 3
         },
         QProcessTask(...) // Process 4
     };

In the above example, all subgroups of a root group define their setup handlers. The following scenario assumes that all started processes finish with success:

Scenario | Comment
--- | ---
Root Group starts | Doesn't return SetupResult, so its tasks are executed.
Group 1 starts | Returns Continue, so its tasks are executed.
Process 1 starts |
... | ...
Process 1 finishes (success) |
Group 1 finishes (success) |
Group 2 starts | Returns StopWithSuccess, so Process 2 is skipped and Group 2 reports success.
Group 2 finishes (success) |
Group 3 starts | Returns StopWithError, so Process 3 is skipped and Group 3 reports an error.
Group 3 finishes (error) |
Root Group finishes (error) | Group 3, which is a direct child of the root group, finished with an error, so the root group stops executing, skips Process 4, which has not started yet, and reports an error.

#### Groups's Done Handler

A Group's done handler is executed after the successful or failed execution of its tasks. The final value reported by the group depends on its Workflow Policy. The handler can apply other necessary actions. The done handler is defined inside the onGroupDone() element of a group. It may take the optional DoneWith argument, indicating the successful or failed execution:

     const Group root {
         onGroupSetup([] { qDebug() << "Root setup"; }),
         QProcessTask(...),
         onGroupDone([](DoneWith result) {
             if (result == DoneWith::Success)
                 qDebug() << "Root finished with success";
             else
                 qDebug() << "Root finished with an error";
         })
     };

The group done handler is optional. If you add more than one onGroupDone() to a group, an assert is triggered at runtime that includes an error message.

Note: Even if the group setup handler returns StopWithSuccess or StopWithError, the group's done handler is invoked. This behavior differs from that of task done handler and might change in the future.

### Other Group Elements

A group can contain other elements that describe the processing flow, such as the execution mode or workflow policy. It can also contain storage elements that are responsible for collecting and sharing custom common data gathered during group execution.

#### Execution Mode

The execution mode element in a Group specifies how the direct child tasks of the Group are started. The most common execution modes are sequential and parallel. It's also possible to specify the limit of tasks running in parallel by using the ParallelLimit element.

In all execution modes, a group starts tasks in the oder in which they appear.

If a child of a group is also a group, the child group runs its tasks according to its own execution mode.

#### Workflow Policy

The workflow policy element in a Group specifies how the group should behave when any of its direct child's tasks finish. For a detailed description of possible policies, refer to WorkflowPolicy.

If a child of a group is also a group, the child group runs its tasks according to its own workflow policy.

#### Storage

Use the Storage element to exchange information between tasks. Especially, in the sequential execution mode, when a task needs data from another, already finished task, before it can start. For example, a task tree that copies data by reading it from a source and writing it to a destination might look as follows:

     static QByteArray load(const QString &fileName) { ... }
     static void save(const QString &fileName, const QByteArray &array) { ... }

     static Group copyRecipe(const QString &source, const QString &destination)
     {
         struct CopyStorage { // [1] custom inter-task struct
             QByteArray content; // [2] custom inter-task data
         };

         // [3] instance of custom inter-task struct manageable by task tree
         const Storage<CopyStorage> storage;

         const auto onLoaderSetup = [source](QThreadFunction<QByteArray> &async) {
             async.setThreadFunctionData(&load, source);
         };
         // [4] runtime: task tree activates the instance from [7] before invoking handler
         const auto onLoaderDone = [storage](const QThreadFunction<QByteArray> &async) {
             storage->content = async.result(); // [5] loader stores the result in storage
         };

         // [4] runtime: task tree activates the instance from [7] before invoking handler
         const auto onSaverSetup = [storage, destination](QThreadFunction<void> &async) {
             const QByteArray content = storage->content; // [6] saver takes data from storage
             async.setThreadFunctionData(&save, destination, content);
         };
         const auto onSaverDone = [](const QThreadFunction<void> &async) {
             qDebug() << "Save done successfully";
         };

         const Group root {
             // [7] runtime: task tree creates an instance of CopyStorage when root is entered
             storage,
             QThreadFunctionTask<QByteArray>(onLoaderSetup, onLoaderDone, CallDoneFlag::OnSuccess),
             QThreadFunctionTask<void>(onSaverSetup, onSaverDone, CallDoneFlag::OnSuccess)
         };
         return root;
     }

     ...

     const QString source = ...;
     const QString destination = ...;
     QTaskTree taskTree(copyRecipe(source, destination));
     connect(&taskTree, &QTaskTree::done, &taskTree, [](DoneWith result) {
         if (result == DoneWith::Success)
             qDebug() << "The copying finished successfully.";
     });
     tasktree.start();

In the example above, the inter-task data consists of a QByteArray content variable [2] enclosed in a `CopyStorage` custom struct [1]. If the loader finishes successfully, it stores the data in a `CopyStorage::content` variable [5]. The saver then uses the variable to configure the saving task [6].

To enable a task tree to manage the `CopyStorage` struct, an instance of Storage<`CopyStorage`> is created [3]. If a copy of this object is inserted as the group's child item [7], an instance of the `CopyStorage` struct is created dynamically when the task tree enters this group. When the task tree leaves this group, the existing instance of the `CopyStorage` struct is destructed as it's no longer needed.

If several task trees holding a copy of the common Storage<`CopyStorage`> instance run simultaneously (including the case when the task trees are run in different threads), each task tree contains its own copy of the `CopyStorage` struct.

You can access `CopyStorage` from any handler in the group with a storage object. This includes all handlers of all descendant tasks of the group with a storage object. To access the custom struct in a handler, pass the copy of the Storage<`CopyStorage`> object to the handler (for example, in a lambda capture) [4].

When the task tree invokes a handler in a subtree containing the storage [7], the task tree activates its own `CopyStorage` instance inside the Storage<`CopyStorage`> object. Therefore, the `CopyStorage` struct may be accessed only from within the handler body. To access the currently active `CopyStorage` from within Storage<`CopyStorage`>, use the Storage::operator->(), Storage::operator*(), or Storage::activeStorage() method.

The following list summarizes how to employ a Storage object into the task tree:

1. Define the custom structure `MyStorage` with custom data [1], [2]
2. Create an instance of the Storage<`MyStorage`> storage [3]
3. Pass the Storage<`MyStorage`> instance to handlers [4]
4. Access the `MyStorage` instance in handlers [5], [6]
5. Insert the Storage<`MyStorage`> instance into a group [7]

### QTaskTree class

QTaskTree executes the tree structure of asynchronous tasks according to the recipe described by the Group root element.

As QTaskTree is also an asynchronous task, it can be a part of another QTaskTree. To place a nested QTaskTree inside another QTaskTree, insert the QTaskTreeTask element into another Group element.

QTaskTree reports progress of completed tasks when running. The progress value is increased when a task finishes or is skipped or canceled. When QTaskTree is finished and the QTaskTree::done() signal is emitted, the current value of the progress equals the maximum progress value. Maximum progress equals the total number of asynchronous tasks in a tree. A nested QTaskTree is counted as a single task, and its child tasks are not counted in the top level tree. Groups themselves are not counted as tasks, but their tasks are counted. QSyncTask tasks are not asynchronous, so they are not counted as tasks.

To set additional initial data for the running tree, modify the storage instances in a tree when it creates them by installing a storage setup handler:

     Storage<CopyStorage> storage;
     const Group root = ...; // storage placed inside root's group and inside handlers
     QTaskTree taskTree(root);
     auto initStorage = [](CopyStorage &storage) {
         storage.content = "initial content";
     };
     taskTree.onStorageSetup(storage, initStorage);
     taskTree.start();

When the running task tree creates a `CopyStorage` instance, and before any handler inside a tree is called, the task tree calls the `initStorage` handler, to enable setting up initial data of the storage, unique to this particular run of `taskTree`.

Similarly, to collect some additional result data from the running tree, read it from storage instances in the tree when they are about to be destroyed. To do this, install a storage done handler:

     Storage<CopyStorage> storage;
     const Group root = ...; // storage placed inside root's group and inside handlers
     QTaskTree taskTree(root);
     auto collectStorage = [](const CopyStorage &storage) {
         qDebug() << "final content" << storage.content;
     };
     taskTree.onStorageDone(storage, collectStorage);
     taskTree.start();

When the running task tree is about to destroy a `CopyStorage` instance, the task tree calls the collectStorage handler, to enable reading the final data from the storage, unique to this particular run of `taskTree`.

### Task Adapters

Allowing new Task types to be a part of recipes is quite easy. It's enough to define a new task alias to the QCustomTask template, passing your `Task` type as a first template argument, like:

     class Worker : public QObject
     {
     public:
         void start() { ... }

     signals:
         void done(bool result);
     };

     using WorkerTask = QCustomTask<Worker>;

This is going to work, if the following conditions are met:

1. Your task is derived from QObject.
2. Your task has public start() method that starts the task.
3. Your task emits done(bool) or done(DoneResult) signal when it's finished.

If your task doesn't meet these conditions, you may still adapt your task to work with the TaskTree framework, by providing a second template argument with the custom adapter. Let's say, we want to adapt QTimer to work with TaskTree. The `Adapter` could look like:

     class TimerAdapter
     {
     public:
         void operator()(QTimer *task, QTaskInterface *iface) {
             task->setSingleShot(true);
             QObject::connect(task, &QTimer::timeout, iface, [iface] {
                 iface->reportDone(DoneResult::Success);
             });
             task->start();
         }
     };

     using TimerTask = QCustomTask<QTimer, TimerAdapter>;

Now you may start using the `TimerTask` in your recipes, like:

     const auto onSetup = [](QTimer &task) {
         task.setInterval(2000);
     };

     const auto onDone = [](const QTimer &task) {
         qDebug() << "Timer triggered after" << task.interval() << "ms.";
     };

     const Group recipe {
         TimerTask(onSetup, onDone)
     };

Note: The class implementing the running task should have a default constructor, and objects of this class should be freely destructible. It should be allowed to destroy a running task, preferably without waiting for the running task to finish (that is, safe non-blocking destructor of a running task). To achieve a non-blocking destruction of a task that has a blocking destructor, consider using the optional `Deleter` template parameter of the QCustomTask (the third template argument).

### Task Tree Runners

The task tree runner manages the lifetime of the underlying QTaskTree used to execute the given recipe.

The following table summarizes the differences between various task tree runners:

Class name | Description
--- | ---
QSingleTaskTreeRunner | Manages single task tree execution. The QSingleTaskTreeRunner::start() method unconditionally starts the passed recipe, resetting any task tree that might be running. Only one task tree can be executing at a time.
QSequentialTaskTreeRunner | Manages sequential task tree executions. The QSequentialTaskTreeRunner::enqueue() method starts the passed recipe if the task tree runner is idle. Otherwise, the recipe is enqueued. When the current task finishes, the runner executes the dequeued recipe sequentially. Only one task tree can be executing at a time.
QParallelTaskTreeRunner | Manages parallel task tree executions. The QParallelTaskTreeRunner::start() method unconditionally starts the passed recipe and keeps any possibly running task trees in parallel.
QMappedTaskTreeRunner | Manages mapped task tree executions. The QMappedTaskTreeRunner::start() method unconditionally starts the specified recipe for a given key. If you already have a different task tree with the same key running, it will be reset. Task trees with different keys are unaffected and continue their execution.