Qt TaskTree C++ Classes

Contains a general purpose TaskTree library. More...

Namespaces

QtTaskTree

Encloses helper classes and global functions of the TaskTree module

Classes

QAbstractTaskTreeRunner

An abstract base class for various task tree controllers

QBarrier

An asynchronous task that finishes on demand

QConcurrentCall

A class template controlling the execution of a function in a separate thread via QtConcurrent::run()

QConcurrentCallBase

A base class for QConcurrentCall class template

QCustomTask

A class template used for declaring custom task items and defining their setup and done handlers

QDefaultTaskAdapter

A class template providing default task adapter used in QCustomTask

QMappedTaskTreeRunner

A mapped task tree execution controller with a given Key type

QNetworkReplyWrapper

A wrapper around QNetworkReply and QNetworkAccessManager

QParallelTaskTreeRunner

A parallel task tree execution controller

QProcessDeleter

A custom deleter for QProcess, used by QProcessTask

QSequentialTaskTreeRunner

A sequential task tree execution controller

QSingleTaskTreeRunner

A single task tree execution controller

QStartedBarrier

A started QBarrier with a given Limit

QSyncTask

Synchronously executes a custom handler between other tasks

QTaskInterface

Helper class used when adapting custom task's interface

QTaskTree

Runs the tree of asynchronous tasks defined in a declarative way

QTcpSocketWrapper

A wrapper around QTcpSocket

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::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::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 QConcurrentCall<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:

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
QProcessTaskQProcessStarts process.
QConcurrentCallTask<ReturnType>QConcurrentCall<ReturnType>Starts asynchronous task, runs in separate thread.
QTaskTreeTaskQTaskTreeStarts nested task tree.
QNetworkReplyWrapperTaskQNetworkReplyWrapperStarts network download.
QTcpSocketWrapperTaskQTcpSocketWrapperStarts 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 QConcurrentCallTask<ReturnType>:

const Group recipe {
    QProcessTask(...),
    QNetworkReplyWrapperTask(...),
    QConcurrentCallTask<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 QConcurrentCallTask<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 QConcurrentCall<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(...),
        QConcurrentCallTask<int>(...)
    },
    QNetworkReplyWrapperTask(...)
};

The example above differs from the first example in that the top level element has a subgroup that contains the QProcessTask and QConcurrentCallTask<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 QConcurrentCall<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 QConcurrentCall<int>), the following scenarios can take place:

Scenario 1Scenario 2
Root Group startsRoot Group starts
Sub Group startsSub Group starts
QProcess startsQProcess starts
QConcurrentCall<int> startsQConcurrentCall<int> starts
......
QProcess finishesQConcurrentCall<int> finishes
......
QConcurrentCall<int> finishesQProcess finishes
Sub Group finishesSub Group finishes
QNetworkReplyWrapper startsQNetworkReplyWrapper starts
......
QNetworkReplyWrapper finishesQNetworkReplyWrapper finishes
Root Group finishesRoot 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 QConcurrentCall<int> is still being executed, the QConcurrentCall<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 ValueBrief Description
ContinueThe 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).
StopWithSuccessThe task won't be started and it will report success to its parent.
StopWithErrorThe 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:

ScenarioComment
Root Group startsDoesn't return SetupResult, so its tasks are executed.
Group 1 startsReturns Continue, so its tasks are executed.
Process 1 starts
......
Process 1 finishes (success)
Group 1 finishes (success)
Group 2 startsReturns StopWithSuccess, so Process 2 is skipped and Group 2 reports success.
Group 2 finishes (success)
Group 3 startsReturns 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() function.

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](QConcurrentCall<QByteArray> &async) {
        async.setConcurrentCallData(&load, source);
    };
    // [4] runtime: task tree activates the instance from [7] before invoking handler
    const auto onLoaderDone = [storage](const QConcurrentCall<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](QConcurrentCall<void> &async) {
        const QByteArray content = storage->content; // [6] saver takes data from storage
        async.setConcurrentCallData(&save, destination, content);
    };
    const auto onSaverDone = [](const QConcurrentCall<void> &async) {
        qDebug() << "Save done successfully";
    };

    const Group root {
        // [7] runtime: task tree creates an instance of CopyStorage when root is entered
        storage,
        QConcurrentCallTask<QByteArray>(onLoaderSetup, onLoaderDone, CallDone::OnSuccess),
        QConcurrentCallTask<void>(onSaverSetup, onSaverDone, CallDone::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);
        });
    }
};

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

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.