summaryrefslogtreecommitdiffstats
path: root/src/assets/downloader/tasking/tasktree.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/assets/downloader/tasking/tasktree.cpp')
-rw-r--r--src/assets/downloader/tasking/tasktree.cpp3701
1 files changed, 0 insertions, 3701 deletions
diff --git a/src/assets/downloader/tasking/tasktree.cpp b/src/assets/downloader/tasking/tasktree.cpp
deleted file mode 100644
index 37064a3e714..00000000000
--- a/src/assets/downloader/tasking/tasktree.cpp
+++ /dev/null
@@ -1,3701 +0,0 @@
-// Copyright (C) 2024 Jarek Kobus
-// Copyright (C) 2024 The Qt Company Ltd.
-// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
-
-#include "tasktree.h"
-
-#include "barrier.h"
-#include "conditional.h"
-
-#include <QtCore/QDebug>
-#include <QtCore/QEventLoop>
-#include <QtCore/QFutureWatcher>
-#include <QtCore/QHash>
-#include <QtCore/QMetaEnum>
-#include <QtCore/QMutex>
-#include <QtCore/QPointer>
-#include <QtCore/QPromise>
-#include <QtCore/QSet>
-#include <QtCore/QTime>
-#include <QtCore/QTimer>
-
-using namespace Qt::StringLiterals;
-using namespace std::chrono;
-
-QT_BEGIN_NAMESPACE
-
-namespace Tasking {
-
-// That's cut down qtcassert.{c,h} to avoid the dependency.
-#define QT_STRING(cond) qDebug("SOFT ASSERT: \"%s\" in %s: %s", cond, __FILE__, QT_STRINGIFY(__LINE__))
-#define QT_ASSERT(cond, action) if (Q_LIKELY(cond)) {} else { QT_STRING(#cond); action; } do {} while (0)
-#define QT_CHECK(cond) if (cond) {} else { QT_STRING(#cond); } do {} while (0)
-
-class Guard
-{
- Q_DISABLE_COPY(Guard)
-public:
- Guard() = default;
- ~Guard() { QT_CHECK(m_lockCount == 0); }
- bool isLocked() const { return m_lockCount; }
-private:
- int m_lockCount = 0;
- friend class GuardLocker;
-};
-
-class GuardLocker
-{
- Q_DISABLE_COPY(GuardLocker)
-public:
- GuardLocker(Guard &guard) : m_guard(guard) { ++m_guard.m_lockCount; }
- ~GuardLocker() { --m_guard.m_lockCount; }
-private:
- Guard &m_guard;
-};
-
-/*!
- \module TaskingSolution
- \title Tasking Solution
- \ingroup solutions-modules
- \brief Contains a general purpose Tasking solution.
-
- The Tasking solution depends on Qt only, and doesn't depend on any \QC specific code.
-*/
-
-/*!
- \namespace Tasking
- \inmodule TaskingSolution
- \brief The Tasking namespace encloses all classes and global functions of the Tasking solution.
-*/
-
-/*!
- \class Tasking::TaskInterface
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief TaskInterface is the abstract base class for implementing custom task adapters.
- \reentrant
-
- To implement a custom task adapter, derive your adapter from the
- \c TaskAdapter<Task> class template. TaskAdapter automatically creates and destroys
- the custom task instance and associates the adapter with a given \c Task type.
-*/
-
-/*!
- \fn virtual void TaskInterface::start()
-
- This method is called by the running TaskTree for starting the \c Task instance.
- Reimplement this method in \c TaskAdapter<Task>'s subclass in order to start the
- associated task.
-
- Use TaskAdapter::task() to access the associated \c Task instance.
-
- \sa done(), TaskAdapter::task()
-*/
-
-/*!
- \fn void TaskInterface::done(DoneResult result)
-
- Emit this signal from the \c TaskAdapter<Task>'s subclass, when the \c Task is finished.
- Pass DoneResult::Success as a \a result argument when the task finishes with success;
- otherwise, when an error occurs, pass DoneResult::Error.
-*/
-
-/*!
- \class Tasking::TaskAdapter
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief A class template for implementing custom task adapters.
- \reentrant
-
- The TaskAdapter class template is responsible for creating a task of the \c Task type,
- starting it, and reporting success or an error when the task is finished.
- It also associates the adapter with a given \c Task type.
-
- Reimplement this class with the actual \c Task type to adapt the task's interface
- into the general TaskTree's interface for managing the \c Task instances.
-
- Each subclass needs to provide a public default constructor,
- implement the start() method, and emit the done() signal when the task is finished.
- Use task() to access the associated \c Task instance.
-
- To use your task adapter inside the task tree, create an alias to the
- Tasking::CustomTask template passing your task adapter as a template parameter:
- \code
- // Defines actual worker
- class Worker {...};
-
- // Adapts Worker's interface to work with task tree
- class WorkerTaskAdapter : public TaskAdapter<Worker> {...};
-
- // Defines WorkerTask as a new custom task type to be placed inside Group items
- using WorkerTask = CustomTask<WorkerTaskAdapter>;
- \endcode
-
- Optionally, you may pass a custom \c Deleter for the associated \c Task
- as a second template parameter of your \c TaskAdapter subclass.
- When the \c Deleter parameter is omitted, the \c std::default_delete<Task> is used by default.
- The custom \c Deleter is useful when the destructor of the running \c Task
- may potentially block the caller thread. Instead of blocking, the custom deleter may move
- the running task into a separate thread and implement the blocking destruction there.
- In this way, the fast destruction (seen from the caller thread) of the running task
- with a blocking destructor may be achieved.
-
- For more information on implementing the custom task adapters, refer to \l {Task Adapters}.
-
- \sa start(), done(), task()
-*/
-
-/*!
- \fn template <typename Task, typename Deleter = std::default_delete<Task>> TaskAdapter<Task, Deleter>::TaskAdapter<Task, Deleter>()
-
- Creates a task adapter for the given \c Task type.
-
- Internally, it creates an instance of \c Task, which is accessible via the task() method.
- The optionally provided \c Deleter is used instead of the \c Task destructor.
- When \c Deleter is omitted, the \c std::default_delete<Task> is used by default.
-
- \sa task()
-*/
-
-/*!
- \fn template <typename Task, typename Deleter = std::default_delete<Task>> Task *TaskAdapter<Task, Deleter>::task()
-
- Returns the pointer to the associated \c Task instance.
-*/
-
-/*!
- \fn template <typename Task, typename Deleter = std::default_delete<Task>> Task *TaskAdapter<Task, Deleter>::task() const
- \overload
-
- Returns the \c const pointer to the associated \c Task instance.
-*/
-
-/*!
- \class Tasking::Storage
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief A class template for custom data exchange in the running task tree.
- \reentrant
-
- The Storage class template is responsible for dynamically creating and destructing objects
- of the custom \c StorageStruct type. The creation and destruction are managed by the
- running task tree. If a Storage object is placed inside a \l {Tasking::Group} {Group} element,
- the running task tree creates the \c StorageStruct object when the group is started and before
- the group's setup handler is called. Later, whenever any handler inside this group is called,
- the task tree activates the previously created instance of the \c StorageStruct object.
- This includes all tasks' and groups' setup and done handlers inside the group where the
- Storage object was placed, also within the nested groups.
- When a copy of the Storage object is passed to the handler via the lambda capture,
- the handler may access the instance activated by the running task tree via the
- \l {Tasking::Storage::operator->()} {operator->()},
- \l {Tasking::Storage::operator*()} {operator*()}, or activeStorage() method.
- If two handlers capture the same Storage object, one of them may store a custom data there,
- and the other may read it afterwards.
- When the group is finished, the previously created instance of the \c StorageStruct
- object is destroyed after the group's done handler is called.
-
- An example of data exchange between tasks:
-
- \code
- const Storage<QString> storage;
-
- const auto onFirstDone = [storage](const Task &task) {
- // Assings QString, taken from the first task result, to the active QString instance
- // of the Storage object.
- *storage = task.getResultAsString();
- };
-
- const auto onSecondSetup = [storage](Task &task) {
- // Reads QString from the active QString instance of the Storage object and use it to
- // configure the second task before start.
- task.configureWithString(*storage);
- };
-
- const Group root {
- // The running task tree creates QString instance when root in entered
- storage,
- // The done handler of the first task stores the QString in the storage
- TaskItem(..., onFirstDone),
- // The setup handler of the second task reads the QString from the storage
- TaskItem(onSecondSetup, ...)
- };
- \endcode
-
- Since the root group executes its tasks sequentially, the \c onFirstDone handler
- is always called before the \c onSecondSetup handler. This means that the QString data,
- read from the \c storage inside the \c onSecondSetup handler's body,
- has already been set by the \c onFirstDone handler.
- You can always rely on it in \l {Tasking::sequential} {sequential} execution mode.
-
- The Storage internals are shared between all of its copies. That is why the copies of the
- Storage object inside the handlers' lambda captures still refer to the same Storage instance.
- You may place multiple Storage objects inside one \l {Tasking::Group} {Group} element,
- provided that they do not include copies of the same Storage object.
- Otherwise, an assert is triggered at runtime that includes an error message.
- However, you can place copies of the same Storage object in different
- \l {Tasking::Group} {Group} elements of the same recipe. In this case, the running task
- tree will create multiple instances of the \c StorageStruct objects (one for each copy)
- and storage shadowing will take place. Storage shadowing works in a similar way
- to C++ variable shadowing inside the nested blocks of code:
-
- \code
- Storage<QString> storage;
-
- const Group root {
- storage, // Top copy, 1st instance of StorageStruct
- onGroupSetup([storage] { ... }), // Top copy is active
- Group {
- storage, // Nested copy, 2nd instance of StorageStruct,
- // shadows Top copy
- onGroupSetup([storage] { ... }), // Nested copy is active
- },
- Group {
- onGroupSetup([storage] { ... }), // Top copy is active
- }
- };
- \endcode
-
- The Storage objects may also be used for passing the initial data to the executed task tree,
- and for reading the final data out of the task tree before it finishes.
- To do this, use \l {TaskTree::onStorageSetup()} {onStorageSetup()} or
- \l {TaskTree::onStorageDone()} {onStorageDone()}, respectively.
-
- \note If you use an unreachable Storage object inside the handler,
- because you forgot to place the storage in the recipe,
- or placed it, but not in any handler's ancestor group,
- you may expect a crash, preceded by the following message:
- \e {The referenced storage is not reachable in the running tree.
- A nullptr will be returned which might lead to a crash in the calling code.
- It is possible that no storage was added to the tree,
- or the storage is not reachable from where it is referenced.}
-*/
-
-/*!
- \fn template <typename StorageStruct> Storage<StorageStruct>::Storage<StorageStruct>()
-
- Creates a storage for the given \c StorageStruct type.
-
- \note All copies of \c this object are considered to be the same Storage instance.
-*/
-
-/*!
- \fn template <typename StorageStruct> StorageStruct &Storage<StorageStruct>::operator*() const noexcept
-
- Returns a \e reference to the active \c StorageStruct object, created by the running task tree.
- Use this function only from inside the handler body of any GroupItem element placed
- in the recipe, otherwise you may expect a crash.
- Make sure that Storage is placed in any group ancestor of the handler's group item.
-
- \note The returned reference is valid as long as the group that created this instance
- is still running.
-
- \sa activeStorage(), operator->()
-*/
-
-/*!
- \fn template <typename StorageStruct> StorageStruct *Storage<StorageStruct>::operator->() const noexcept
-
- Returns a \e pointer to the active \c StorageStruct object, created by the running task tree.
- Use this function only from inside the handler body of any GroupItem element placed
- in the recipe, otherwise you may expect a crash.
- Make sure that Storage is placed in any group ancestor of the handler's group item.
-
- \note The returned pointer is valid as long as the group that created this instance
- is still running.
-
- \sa activeStorage(), operator*()
-*/
-
-/*!
- \fn template <typename StorageStruct> StorageStruct *Storage<StorageStruct>::activeStorage() const
-
- Returns a \e pointer to the active \c StorageStruct object, created by the running task tree.
- Use this function only from inside the handler body of any GroupItem element placed
- in the recipe, otherwise you may expect a crash.
- Make sure that Storage is placed in any group ancestor of the handler's group item.
-
- \note The returned pointer is valid as long as the group that created this instance
- is still running.
-
- \sa operator->(), operator*()
-*/
-
-/*!
- \typealias Tasking::GroupItems
-
- Type alias for QList<GroupItem>.
-*/
-
-/*!
- \class Tasking::GroupItem
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief GroupItem represents the basic element that may be a part of any Group.
- \reentrant
-
- GroupItem is a basic element that may be a part of any \l {Tasking::Group} {Group}.
- It encapsulates the functionality provided by any GroupItem's subclass.
- It is a value type and it is safe to copy the GroupItem instance,
- even when it is originally created via the subclass' constructor.
-
- There are four main kinds of GroupItem:
- \table
- \header
- \li GroupItem Kind
- \li Brief Description
- \row
- \li \l CustomTask
- \li Defines asynchronous task type and task's start, done, and error handlers.
- Aliased with a unique task name, such as, \c ConcurrentCallTask<ResultType>
- or \c NetworkQueryTask. Asynchronous tasks are the main reason for using a task tree.
- \row
- \li \l {Tasking::Group} {Group}
- \li A container for other group items. Since the group is of the GroupItem type,
- it's possible to nest it inside another group. The group is seen by its parent
- as a single asynchronous task.
- \row
- \li GroupItem containing \l {Tasking::Storage} {Storage}
- \li Enables the child tasks of a group to exchange data. When GroupItem containing
- \l {Tasking::Storage} {Storage} is placed inside a group, the task tree instantiates
- the storage's data object just before the group is entered,
- and destroys it just after the group is left.
- \row
- \li Other group control items
- \li The items returned by \l {Tasking::parallelLimit()} {parallelLimit()} or
- \l {Tasking::workflowPolicy()} {workflowPolicy()} influence the group's behavior.
- The items returned by \l {Tasking::onGroupSetup()} {onGroupSetup()} or
- \l {Tasking::onGroupDone()} {onGroupDone()} define custom handlers called when
- the group starts or ends execution.
- \endtable
-*/
-
-/*!
- \fn template <typename StorageStruct> GroupItem::GroupItem(const Storage<StorageStruct> &storage)
-
- Constructs a \c GroupItem element holding the \a storage object.
-
- When the \l {Tasking::Group} {Group} element containing \e this GroupItem is entered
- by the running task tree, an instance of the \c StorageStruct is created dynamically.
-
- When that group is about to be left after its execution, the previously instantiated
- \c StorageStruct is deleted.
-
- The dynamically created instance of \c StorageStruct is accessible from inside any
- handler body of the parent \l {Tasking::Group} {Group} element,
- including nested groups and its tasks, via the
- \l {Tasking::Storage::operator->()} {Storage::operator->()},
- \l {Tasking::Storage::operator*()} {Storage::operator*()}, or Storage::activeStorage() method.
-
- \sa {Tasking::Storage} {Storage}
-*/
-
-/*!
- \fn GroupItem::GroupItem(const GroupItems &items)
-
- Constructs a \c GroupItem element with a given list of \a items.
-
- When this \c GroupItem element is parsed by the TaskTree, it is simply replaced with
- its \a items.
-
- This constructor is useful when constructing a \l {Tasking::Group} {Group} element with
- lists of \c GroupItem elements:
-
- \code
- static QList<GroupItems> getItems();
-
- ...
-
- const Group root {
- parallel,
- finishAllAndSuccess,
- getItems(), // OK, getItems() list is wrapped into a single GroupItem element
- onGroupSetup(...),
- onGroupDone(...)
- };
- \endcode
-
- If you want to create a subtree, use \l {Tasking::Group} {Group} instead.
-
- \note Don't confuse this \c GroupItem with the \l {Tasking::Group} {Group} element, as
- \l {Tasking::Group} {Group} keeps its children nested
- after being parsed by the task tree, while this \c GroupItem does not.
-
- \sa {Tasking::Group} {Group}
-*/
-
-/*!
- \fn Tasking::GroupItem(std::initializer_list<GroupItem> items)
- \overload
- \sa GroupItem(const GroupItems &items)
-*/
-
-/*!
- \class Tasking::Group
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief Group represents the basic element for composing declarative recipes describing
- how to execute and handle a nested tree of asynchronous tasks.
- \reentrant
-
- Group is a container for other group items. It encloses child tasks into one unit,
- which is seen by the group's parent as a single, asynchronous task.
- Since Group is of the GroupItem type, it may also be a child of Group.
-
- Insert child tasks into the group by using aliased custom task names, such as,
- \c ConcurrentCallTask<ResultType> or \c NetworkQueryTask:
-
- \code
- const Group group {
- NetworkQueryTask(...),
- ConcurrentCallTask<int>(...)
- };
- \endcode
-
- The group's behavior may be customized by inserting the items returned by
- \l {Tasking::parallelLimit()} {parallelLimit()} or
- \l {Tasking::workflowPolicy()} {workflowPolicy()} functions:
-
- \code
- const Group group {
- parallel,
- continueOnError,
- NetworkQueryTask(...),
- NetworkQueryTask(...)
- };
- \endcode
-
- The group may contain nested groups:
-
- \code
- const Group group {
- finishAllAndSuccess,
- NetworkQueryTask(...),
- Group {
- NetworkQueryTask(...),
- Group {
- parallel,
- NetworkQueryTask(...),
- NetworkQueryTask(...),
- }
- ConcurrentCallTask<QString>(...)
- }
- };
- \endcode
-
- The group may dynamically instantiate a custom storage structure, which may be used for
- inter-task data exchange:
-
- \code
- struct MyCustomStruct { QByteArray data; };
-
- Storage<MyCustomStruct> storage;
-
- const auto onFirstSetup = [](NetworkQuery &task) { ... };
- const auto onFirstDone = [storage](const NetworkQuery &task) {
- // storage-> gives a pointer to MyCustomStruct instance,
- // created dynamically by the running task tree.
- storage->data = task.reply()->readAll();
- };
- const auto onSecondSetup = [storage](ConcurrentCall<QImage> &task) {
- // storage-> gives a pointer to MyCustomStruct. Since the group is sequential,
- // the stored MyCustomStruct was already updated inside the onFirstDone handler.
- const QByteArray storedData = storage->data;
- };
-
- const Group group {
- // When the group is entered by a running task tree, it creates MyCustomStruct
- // instance dynamically. It is later accessible from all handlers via
- // the *storage or storage-> operators.
- sequential,
- storage,
- NetworkQueryTask(onFirstSetup, onFirstDone, CallDoneIf::Success),
- ConcurrentCallTask<QImage>(onSecondSetup)
- };
- \endcode
-*/
-
-/*!
- \fn Group::Group(const GroupItems &children)
-
- Constructs a group with a given list of \a children.
-
- This constructor is useful when the child items of the group are not known at compile time,
- but later, at runtime:
-
- \code
- const QStringList sourceList = ...;
-
- GroupItems groupItems { parallel };
-
- for (const QString &source : sourceList) {
- const NetworkQueryTask task(...); // use source for setup handler
- groupItems << task;
- }
-
- const Group group(groupItems);
- \endcode
-*/
-
-/*!
- \fn Group::Group(std::initializer_list<GroupItem> children)
-
- Constructs a group from \c std::initializer_list given by \a children.
-
- This constructor is useful when all child items of the group are known at compile time:
-
- \code
- const Group group {
- finishAllAndSuccess,
- NetworkQueryTask(...),
- Group {
- NetworkQueryTask(...),
- Group {
- parallel,
- NetworkQueryTask(...),
- NetworkQueryTask(...),
- }
- ConcurrentCallTask<QString>(...)
- }
- };
- \endcode
-*/
-
-/*!
- \class Tasking::Sync
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief Synchronously executes a custom handler between other tasks.
- \reentrant
-
- \c Sync is useful when you want to execute an additional handler between other tasks.
- \c Sync is seen by its parent \l {Tasking::Group} {Group} as any other task.
- Avoid long-running execution of the \c Sync's handler body, since it is executed
- synchronously from the caller thread. If that is unavoidable, consider using
- \c ConcurrentCallTask instead.
-*/
-
-/*!
- \fn template <typename Handler> Sync::Sync(Handler &&handler)
-
- Constructs an element that executes a passed \a handler synchronously.
- The \c Handler is of the \c std::function<DoneResult()> type.
- The DoneResult value, returned by the \a handler, is considered during parent group's
- \l {workflowPolicy} {workflow policy} resolution.
- Optionally, the shortened form of \c std::function<void()> is also accepted.
- In this case, it's assumed that the return value is DoneResult::Success.
-
- The passed \a handler executes synchronously from the caller thread, so avoid a long-running
- execution of the handler body. Otherwise, consider using \c ConcurrentCallTask.
-
- \note The \c Sync element is not counted as a task when reporting task tree progress,
- and is not included in TaskTree::taskCount() or TaskTree::progressMaximum().
-*/
-
-/*!
- \class Tasking::CustomTask
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief A class template used for declaring custom task items and defining their setup
- and done handlers.
- \reentrant
-
- Describes custom task items within task tree recipes.
-
- Custom task names are aliased with unique names using the \c CustomTask template
- with a given TaskAdapter subclass as a template parameter.
- For example, \c ConcurrentCallTask<T> is an alias to the \c CustomTask that is defined
- to work with \c ConcurrentCall<T> as an associated task class.
- The following table contains example custom tasks and their associated task classes:
-
- \table
- \header
- \li Aliased Task Name (Tasking Namespace)
- \li Associated Task Class
- \li Brief Description
- \row
- \li ConcurrentCallTask<ReturnType>
- \li ConcurrentCall<ReturnType>
- \li Starts an asynchronous task. Runs in a separate thread.
- \row
- \li NetworkQueryTask
- \li NetworkQuery
- \li Sends a network query.
- \row
- \li TaskTreeTask
- \li TaskTree
- \li Starts a nested task tree.
- \row
- \li TimeoutTask
- \li \c std::chrono::milliseconds
- \li Starts a timer.
- \row
- \li WaitForBarrierTask
- \li MultiBarrier<Limit>
- \li Starts an asynchronous task waiting for the barrier to pass.
- \endtable
-*/
-
-/*!
- \typealias Tasking::CustomTask::Task
-
- Type alias for the task type associated with the custom task's \c Adapter.
-*/
-
-/*!
- \typealias Tasking::CustomTask::Deleter
-
- Type alias for the task's type deleter associated with the custom task's \c Adapter.
-*/
-
-/*!
- \typealias Tasking::CustomTask::TaskSetupHandler
-
- Type alias for \c std::function<SetupResult(Task &)>.
-
- The \c TaskSetupHandler is an optional argument of a custom task element's constructor.
- Any function with the above signature, when passed as a task setup handler,
- will be called by the running task tree after the task is created and before it is started.
-
- Inside the body of the handler, you may configure the task according to your needs.
- The additional parameters, including storages, may be passed to the handler
- via the lambda capture.
- You can decide dynamically whether the task should be started or skipped with
- success or an error.
-
- \note Do not start the task inside the start handler by yourself. Leave it for TaskTree,
- otherwise the behavior is undefined.
-
- The return value of the handler instructs the running task tree on how to proceed
- after the handler's invocation is finished. The return value of SetupResult::Continue
- instructs the task tree to continue running, that is, to execute the associated \c Task.
- The return value of SetupResult::StopWithSuccess or SetupResult::StopWithError
- instructs the task tree to skip the task's execution and finish it immediately with
- success or an error, respectively.
-
- When the return type is either SetupResult::StopWithSuccess or SetupResult::StopWithError,
- the task's done handler (if provided) isn't called afterwards.
-
- The constructor of a custom task accepts also functions in the shortened form of
- \c std::function<void(Task &)>, that is, the return value is \c void.
- In this case, it's assumed that the return value is SetupResult::Continue.
-
- \sa CustomTask(), TaskDoneHandler, GroupSetupHandler
-*/
-
-/*!
- \typealias Tasking::CustomTask::TaskDoneHandler
-
- Type alias for \c std::function<DoneResult(const Task &, DoneWith)> or DoneResult.
-
- The \c TaskDoneHandler is an optional argument of a custom task element's constructor.
- Any function with the above signature, when passed as a task done handler,
- will be called by the running task tree after the task execution finished and before
- the final result of the execution is reported back to the parent group.
-
- Inside the body of the handler you may retrieve the final data from the finished task.
- The additional parameters, including storages, may be passed to the handler
- via the lambda capture.
- It is also possible to decide dynamically whether the task should finish with its return
- value, or the final result should be tweaked.
-
- The DoneWith argument is optional and your done handler may omit it.
- When provided, it holds the info about the final result of a task that will be
- reported to its parent.
-
- If you do not plan to read any data from the finished task,
- you may omit the \c {const Task &} argument.
-
- The returned DoneResult value is optional and your handler may return \c void instead.
- In this case, the final result of the task will be equal to the value indicated by
- the DoneWith argument. When the handler returns the DoneResult value,
- the task's final result may be tweaked inside the done handler's body by the returned value.
-
- For a \c TaskDoneHandler of the DoneResult type, no additional handling is executed,
- and the task finishes unconditionally with the passed value of DoneResult.
-
- \sa CustomTask(), TaskSetupHandler, GroupDoneHandler
-*/
-
-/*!
- \fn template <typename Adapter> template <typename SetupHandler = TaskSetupHandler, typename DoneHandler = TaskDoneHandler> CustomTask<Adapter>::CustomTask(SetupHandler &&setup = TaskSetupHandler(), DoneHandler &&done = TaskDoneHandler(), CallDoneIf callDoneIf = CallDoneIf::SuccessOrError)
-
- Constructs a \c CustomTask instance and attaches the \a setup and \a done handlers to the task.
- When the running task tree is about to start the task,
- it instantiates the associated \l Task object, invokes \a setup handler with a \e reference
- to the created task, and starts it. When the running task finishes,
- the task tree invokes a \a done handler, with a \c const \e reference to the created task.
-
- The passed \a setup handler is of the \l TaskSetupHandler type. For example:
-
- \code
- static void parseAndLog(const QString &input);
-
- ...
-
- const QString input = ...;
-
- const auto onFirstSetup = [input](ConcurrentCall<void> &task) {
- if (input == "Skip")
- return SetupResult::StopWithSuccess; // This task won't start, the next one will
- if (input == "Error")
- return SetupResult::StopWithError; // This task and the next one won't start
- task.setConcurrentCallData(parseAndLog, input);
- // This task will start, and the next one will start after this one finished with success
- return SetupResult::Continue;
- };
-
- const auto onSecondSetup = [input](ConcurrentCall<void> &task) {
- task.setConcurrentCallData(parseAndLog, input);
- };
-
- const Group group {
- ConcurrentCallTask<void>(onFirstSetup),
- ConcurrentCallTask<void>(onSecondSetup)
- };
- \endcode
-
- The \a done handler is of the \l TaskDoneHandler type.
- By default, the \a done handler is invoked whenever the task finishes.
- Pass a non-default value for the \a callDoneIf argument when you want the handler to be called
- only on a successful or failed execution.
-
- \sa TaskSetupHandler, TaskDoneHandler
-*/
-
-/*!
- \enum Tasking::WorkflowPolicy
-
- This enum describes the possible behavior of the Group element when any group's child task
- finishes its execution. It's also used when the running Group is canceled.
-
- \value StopOnError
- Default. Corresponds to the stopOnError global element.
- If any child task finishes with an error, the group stops and finishes with an error.
- If all child tasks finished with success, the group finishes with success.
- If a group is empty, it finishes with success.
- \value ContinueOnError
- Corresponds to the continueOnError global element.
- Similar to stopOnError, but in case any child finishes with an error,
- the execution continues until all tasks finish, and the group reports an error
- afterwards, even when some other tasks in the group finished with success.
- If all child tasks finish successfully, the group finishes with success.
- If a group is empty, it finishes with success.
- \value StopOnSuccess
- Corresponds to the stopOnSuccess global element.
- If any child task finishes with success, the group stops and finishes with success.
- If all child tasks finished with an error, the group finishes with an error.
- If a group is empty, it finishes with an error.
- \value ContinueOnSuccess
- Corresponds to the continueOnSuccess global element.
- Similar to stopOnSuccess, but in case any child finishes successfully,
- the execution continues until all tasks finish, and the group reports success
- afterwards, even when some other tasks in the group finished with an error.
- If all child tasks finish with an error, the group finishes with an error.
- If a group is empty, it finishes with an error.
- \value StopOnSuccessOrError
- Corresponds to the stopOnSuccessOrError global element.
- The group starts as many tasks as it can. When any task finishes,
- the group stops and reports the task's result.
- Useful only in parallel mode.
- In sequential mode, only the first task is started, and when finished,
- the group finishes too, so the other tasks are always skipped.
- If a group is empty, it finishes with an error.
- \value FinishAllAndSuccess
- Corresponds to the finishAllAndSuccess global element.
- The group executes all tasks and ignores their return results. When all
- tasks finished, the group finishes with success.
- If a group is empty, it finishes with success.
- \value FinishAllAndError
- Corresponds to the finishAllAndError global element.
- The group executes all tasks and ignores their return results. When all
- tasks finished, the group finishes with an error.
- If a group is empty, it finishes with an error.
-
- Whenever a child task's result causes the Group to stop, that is,
- in case of StopOnError, StopOnSuccess, or StopOnSuccessOrError policies,
- the Group cancels the other running child tasks (if any - for example in parallel mode),
- and skips executing tasks it has not started yet (for example, in the sequential mode -
- those, that are placed after the failed task). Both canceling and skipping child tasks
- may happen when parallelLimit() is used.
-
- The table below summarizes the differences between various workflow policies:
-
- \table
- \header
- \li \l WorkflowPolicy
- \li Executes all child tasks
- \li Result
- \li Result when the group is empty
- \row
- \li StopOnError
- \li Stops when any child task finished with an error and reports an error
- \li An error when at least one child task failed, success otherwise
- \li Success
- \row
- \li ContinueOnError
- \li Yes
- \li An error when at least one child task failed, success otherwise
- \li Success
- \row
- \li StopOnSuccess
- \li Stops when any child task finished with success and reports success
- \li Success when at least one child task succeeded, an error otherwise
- \li An error
- \row
- \li ContinueOnSuccess
- \li Yes
- \li Success when at least one child task succeeded, an error otherwise
- \li An error
- \row
- \li StopOnSuccessOrError
- \li Stops when any child task finished and reports child task's result
- \li Success or an error, depending on the finished child task's result
- \li An error
- \row
- \li FinishAllAndSuccess
- \li Yes
- \li Success
- \li Success
- \row
- \li FinishAllAndError
- \li Yes
- \li An error
- \li An error
- \endtable
-
- If a child of a group is also a group, the child group runs its tasks according to its own
- workflow policy. When a parent group stops the running child group because
- of parent group's workflow policy, that is, when the StopOnError, StopOnSuccess,
- or StopOnSuccessOrError policy was used for the parent,
- the child group's result is reported according to the
- \b Result column and to the \b {child group's workflow policy} row in the table above.
-*/
-
-/*!
- \variable Tasking::nullItem
-
- A convenient global group's element indicating a no-op item.
-
- This is useful in conditional expressions to indicate the absence of an optional element:
-
- \code
- const ExecutableItem task = ...;
- const std::optional<ExecutableItem> optionalTask = ...;
-
- Group group {
- task,
- optionalTask ? *optionalTask : nullItem
- };
- \endcode
-*/
-
-/*!
- \variable Tasking::successItem
-
- A convenient global executable element containing an empty, successful, synchronous task.
-
- This is useful in if-statements to indicate that a branch ends with success:
-
- \code
- const ExecutableItem conditionalTask = ...;
-
- Group group {
- stopOnDone,
- If (conditionalTask) >> Then {
- ...
- } >> Else {
- successItem
- },
- nextTask
- };
- \endcode
-
- In the above example, if the \c conditionalTask finishes with an error, the \c Else branch
- is chosen, which finishes immediately with success. This causes the \c nextTask to be skipped
- (because of the stopOnDone workflow policy of the \c group)
- and the \c group finishes with success.
-
- \sa errorItem
-*/
-
-/*!
- \variable Tasking::errorItem
-
- A convenient global executable element containing an empty, erroneous, synchronous task.
-
- This is useful in if-statements to indicate that a branch ends with an error:
-
- \code
- const ExecutableItem conditionalTask = ...;
-
- Group group {
- stopOnError,
- If (conditionalTask) >> Then {
- ...
- } >> Else {
- errorItem
- },
- nextTask
- };
- \endcode
-
- In the above example, if the \c conditionalTask finishes with an error, the \c Else branch
- is chosen, which finishes immediately with an error. This causes the \c nextTask to be skipped
- (because of the stopOnError workflow policy of the \c group)
- and the \c group finishes with an error.
-
- \sa successItem
-*/
-
-/*!
- \variable Tasking::sequential
- A convenient global group's element describing the sequential execution mode.
-
- This is the default execution mode of the Group element.
-
- When a Group has no execution mode, it runs in the sequential mode.
- All the direct child tasks of a group are started in a chain, so that when one task finishes,
- the next one starts. This enables you to pass the results from the previous task
- as input to the next task before it starts. This mode guarantees that the next task
- is started only after the previous task finishes.
-
- \sa parallel, parallelLimit()
-*/
-
-/*!
- \variable Tasking::parallel
- A convenient global group's element describing the parallel execution mode.
-
- All the direct child tasks of a group are started after the group is started,
- without waiting for the previous child tasks to finish.
- In this mode, all child tasks run simultaneously.
-
- \sa sequential, parallelLimit()
-*/
-
-/*!
- \variable Tasking::parallelIdealThreadCountLimit
- A convenient global group's element describing the parallel execution mode with a limited
- number of tasks running simultanously. The limit is equal to the ideal number of threads
- excluding the calling thread.
-
- This is a shortcut to:
- \code
- parallelLimit(qMax(QThread::idealThreadCount() - 1, 1))
- \endcode
-
- \sa parallel, parallelLimit()
-*/
-
-/*!
- \variable Tasking::stopOnError
- A convenient global group's element describing the StopOnError workflow policy.
-
- This is the default workflow policy of the Group element.
-*/
-
-/*!
- \variable Tasking::continueOnError
- A convenient global group's element describing the ContinueOnError workflow policy.
-*/
-
-/*!
- \variable Tasking::stopOnSuccess
- A convenient global group's element describing the StopOnSuccess workflow policy.
-*/
-
-/*!
- \variable Tasking::continueOnSuccess
- A convenient global group's element describing the ContinueOnSuccess workflow policy.
-*/
-
-/*!
- \variable Tasking::stopOnSuccessOrError
- A convenient global group's element describing the StopOnSuccessOrError workflow policy.
-*/
-
-/*!
- \variable Tasking::finishAllAndSuccess
- A convenient global group's element describing the FinishAllAndSuccess workflow policy.
-*/
-
-/*!
- \variable Tasking::finishAllAndError
- A convenient global group's element describing the FinishAllAndError workflow policy.
-*/
-
-/*!
- \enum Tasking::SetupResult
-
- This enum is optionally returned from the group's or task's setup handler function.
- It instructs the running task tree on how to proceed after the setup handler's execution
- finished.
- \value Continue
- Default. The group's or task's execution continues normally.
- When a group's or task's setup handler returns void, it's assumed that
- it returned Continue.
- \value StopWithSuccess
- The group's or task's execution stops immediately with success.
- When returned from the group's setup handler, all child tasks are skipped,
- and the group's onGroupDone() handler is invoked with DoneWith::Success.
- The group reports success to its parent. The group's workflow policy is ignored.
- When returned from the task's setup handler, the task isn't started,
- its done handler isn't invoked, and the task reports success to its parent.
- \value StopWithError
- The group's or task's execution stops immediately with an error.
- When returned from the group's setup handler, all child tasks are skipped,
- and the group's onGroupDone() handler is invoked with DoneWith::Error.
- The group reports an error to its parent. The group's workflow policy is ignored.
- When returned from the task's setup handler, the task isn't started,
- its error handler isn't invoked, and the task reports an error to its parent.
-*/
-
-/*!
- \enum Tasking::DoneResult
-
- This enum is optionally returned from the group's or task's done handler function.
- When the done handler doesn't return any value, that is, its return type is \c void,
- its final return value is automatically deduced by the running task tree and reported
- to its parent group.
-
- When the done handler returns the DoneResult, you can tweak the final return value
- inside the handler.
-
- When the DoneResult is returned by the group's done handler, the group's workflow policy
- is ignored.
-
- This enum is also used inside the TaskInterface::done() signal and it indicates whether
- the task finished with success or an error.
-
- \value Success
- The group's or task's execution ends with success.
- \value Error
- The group's or task's execution ends with an error.
-*/
-
-/*!
- \enum Tasking::DoneWith
-
- This enum is an optional argument for the group's or task's done handler.
- It indicates whether the group or task finished with success or an error, or it was canceled.
-
- It is also used as an argument inside the TaskTree::done() signal,
- indicating the final result of the TaskTree execution.
-
- \value Success
- The group's or task's execution ended with success.
- \value Error
- The group's or task's execution ended with an error.
- \value Cancel
- The group's or task's execution was canceled. This happens when the user calls
- TaskTree::cancel() for the running task tree or when the group's workflow policy
- results in canceling some of its running children.
- Tweaking the done handler's final result by returning Tasking::DoneResult from
- the handler is no-op when the group's or task's execution was canceled.
-*/
-
-/*!
- \enum Tasking::CallDoneIf
-
- This enum is an optional argument for the \l onGroupDone() element or custom task's constructor.
- It instructs the task tree on when the group's or task's done handler should be invoked.
-
- \value SuccessOrError
- The done handler is always invoked.
- \value Success
- The done handler is invoked only after successful execution,
- that is, when DoneWith::Success.
- \value Error
- The done handler is invoked only after failed execution,
- that is, when DoneWith::Error or when DoneWith::Cancel.
-*/
-
-/*!
- \typealias Tasking::GroupItem::GroupSetupHandler
-
- Type alias for \c std::function<SetupResult()>.
-
- The \c GroupSetupHandler is an argument of the onGroupSetup() element.
- Any function with the above signature, when passed as a group setup handler,
- will be called by the running task tree when the group execution starts.
-
- The return value of the handler instructs the running group on how to proceed
- after the handler's invocation is finished. The default return value of SetupResult::Continue
- instructs the group to continue running, that is, to start executing its child tasks.
- The return value of SetupResult::StopWithSuccess or SetupResult::StopWithError
- instructs the group to skip the child tasks' execution and finish immediately with
- success or an error, respectively.
-
- When the return type is either SetupResult::StopWithSuccess or SetupResult::StopWithError,
- the group's done handler (if provided) is called synchronously immediately afterwards.
-
- \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.
-
- The onGroupSetup() element accepts also functions in the shortened form of
- \c std::function<void()>, that is, the return value is \c void.
- In this case, it's assumed that the return value is SetupResult::Continue.
-
- \sa onGroupSetup(), GroupDoneHandler, CustomTask::TaskSetupHandler
-*/
-
-/*!
- \typealias Tasking::GroupItem::GroupDoneHandler
-
- Type alias for \c std::function<DoneResult(DoneWith)> or DoneResult.
-
- The \c GroupDoneHandler is an argument of the onGroupDone() element.
- Any function with the above signature, when passed as a group done handler,
- will be called by the running task tree when the group execution ends.
-
- The DoneWith argument is optional and your done handler may omit it.
- When provided, it holds the info about the final result of a group that will be
- reported to its parent.
-
- The returned DoneResult value is optional and your handler may return \c void instead.
- In this case, the final result of the group will be equal to the value indicated by
- the DoneWith argument. When the handler returns the DoneResult value,
- the group's final result may be tweaked inside the done handler's body by the returned value.
-
- For a \c GroupDoneHandler of the DoneResult type, no additional handling is executed,
- and the group finishes unconditionally with the passed value of DoneResult,
- ignoring the group's workflow policy.
-
- \sa onGroupDone(), GroupSetupHandler, CustomTask::TaskDoneHandler
-*/
-
-/*!
- \fn template <typename Handler> GroupItem onGroupSetup(Handler &&handler)
-
- Constructs a group's element holding the group setup handler.
- The \a handler is invoked whenever the group starts.
-
- The passed \a handler is either of the \c std::function<SetupResult()> or the
- \c std::function<void()> type. For more information on a possible handler type, refer to
- \l {GroupItem::GroupSetupHandler}.
-
- When the \a handler is invoked, none of the group's child tasks are running yet.
-
- If a group contains the Storage elements, the \a handler is invoked
- after the storages are constructed, so that the \a handler may already
- perform some initial modifications to the active storages.
-
- \sa GroupItem::GroupSetupHandler, onGroupDone()
-*/
-
-/*!
- \fn template <typename Handler> GroupItem onGroupDone(Handler &&handler, CallDoneIf callDoneIf = CallDoneIf::SuccessOrError)
-
- Constructs a group's element holding the group done handler.
- By default, the \a handler is invoked whenever the group finishes.
- Pass a non-default value for the \a callDoneIf argument when you want the handler to be called
- only on a successful or failed execution.
- Depending on the group's workflow policy, this handler may also be called
- when the running group is canceled (e.g. when stopOnError element was used).
-
- The passed \a handler is of the \c std::function<DoneResult(DoneWith)> type.
- Optionally, each of the return DoneResult type or the argument DoneWith type may be omitted
- (that is, its return type may be \c void). For more information on a possible handler type,
- refer to \l {GroupItem::GroupDoneHandler}.
-
- When the \a handler is invoked, all of the group's child tasks are already finished.
-
- If a group contains the Storage elements, the \a handler is invoked
- before the storages are destructed, so that the \a handler may still
- perform a last read of the active storages' data.
-
- \sa GroupItem::GroupDoneHandler, onGroupSetup()
-*/
-
-/*!
- Constructs a group's element describing the \l{Execution Mode}{execution mode}.
-
- The execution mode element in a Group specifies how the direct child tasks of
- the Group are started.
-
- For convenience, when appropriate, the \l sequential or \l parallel global elements
- may be used instead.
-
- The \a limit defines the maximum number of direct child tasks running in parallel:
-
- \list
- \li When \a limit equals to 0, there is no limit, and all direct child tasks are started
- together, in the oder in which they appear in a group. This means the fully parallel
- execution, and the \l parallel element may be used instead.
-
- \li When \a limit equals to 1, it means that only one child task may run at the time.
- This means the sequential execution, and the \l sequential element may be used instead.
- In this case, child tasks run in chain, so the next child task starts after
- the previous child task has finished.
-
- \li When other positive number is passed as \a limit, the group's child tasks run
- in parallel, but with a limited number of tasks running simultanously.
- The \e limit defines the maximum number of tasks running in parallel in a group.
- When the group is started, the first batch of tasks is started
- (the number of tasks in a batch equals to the passed \a limit, at most),
- while the others are kept waiting. When any running task finishes,
- the group starts the next remaining one, so that the \e limit of simultaneously
- running tasks inside a group isn't exceeded. This repeats on every child task's
- finish until all child tasks are started. This enables you to limit the maximum
- number of tasks that run simultaneously, for example if running too many processes might
- block the machine for a long time.
- \endlist
-
- 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.
-
- \sa sequential, parallel
-*/
-
-GroupItem parallelLimit(int limit)
-{
- struct ParallelLimit : GroupItem {
- ParallelLimit(int limit) : GroupItem({{}, limit}) {}
- };
- return ParallelLimit(limit);
-}
-
-/*!
- Constructs a group's \l {Workflow Policy} {workflow policy} element for a given \a policy.
-
- For convenience, global elements may be used instead.
-
- \sa stopOnError, continueOnError, stopOnSuccess, continueOnSuccess, stopOnSuccessOrError,
- finishAllAndSuccess, finishAllAndError, WorkflowPolicy
-*/
-GroupItem workflowPolicy(WorkflowPolicy policy)
-{
- struct WorkflowPolicyItem : GroupItem {
- WorkflowPolicyItem(WorkflowPolicy policy) : GroupItem({{}, {}, policy}) {}
- };
- return WorkflowPolicyItem(policy);
-}
-
-const GroupItem sequential = parallelLimit(1);
-const GroupItem parallel = parallelLimit(0);
-const GroupItem parallelIdealThreadCountLimit = parallelLimit(qMax(QThread::idealThreadCount() - 1, 1));
-
-const GroupItem stopOnError = workflowPolicy(WorkflowPolicy::StopOnError);
-const GroupItem continueOnError = workflowPolicy(WorkflowPolicy::ContinueOnError);
-const GroupItem stopOnSuccess = workflowPolicy(WorkflowPolicy::StopOnSuccess);
-const GroupItem continueOnSuccess = workflowPolicy(WorkflowPolicy::ContinueOnSuccess);
-const GroupItem stopOnSuccessOrError = workflowPolicy(WorkflowPolicy::StopOnSuccessOrError);
-const GroupItem finishAllAndSuccess = workflowPolicy(WorkflowPolicy::FinishAllAndSuccess);
-const GroupItem finishAllAndError = workflowPolicy(WorkflowPolicy::FinishAllAndError);
-
-// Keep below the above in order to avoid static initialization fiasco.
-const GroupItem nullItem = Group {};
-const ExecutableItem successItem = Group { finishAllAndSuccess };
-const ExecutableItem errorItem = Group { finishAllAndError };
-
-Group operator>>(const For &forItem, const Do &doItem)
-{
- return {forItem.m_loop, doItem.m_children};
-}
-
-Group operator>>(const When &whenItem, const Do &doItem)
-{
- const SingleBarrier barrier;
-
- return {
- barrier,
- parallel,
- whenItem.m_barrierKicker(barrier),
- Group {
- waitForBarrierTask(barrier),
- doItem.m_children
- }
- };
-}
-
-// Please note the thread_local keyword below guarantees a separate instance per thread.
-// The s_activeTaskTrees is currently used internally only and is not exposed in the public API.
-// It serves for withLog() implementation now. Add a note here when a new usage is introduced.
-static thread_local QList<TaskTree *> s_activeTaskTrees = {};
-
-static TaskTree *activeTaskTree()
-{
- QT_ASSERT(s_activeTaskTrees.size(), return nullptr);
- return s_activeTaskTrees.back();
-}
-
-DoneResult toDoneResult(bool success)
-{
- return success ? DoneResult::Success : DoneResult::Error;
-}
-
-static SetupResult toSetupResult(bool success)
-{
- return success ? SetupResult::StopWithSuccess : SetupResult::StopWithError;
-}
-
-static DoneResult toDoneResult(DoneWith doneWith)
-{
- return doneWith == DoneWith::Success ? DoneResult::Success : DoneResult::Error;
-}
-
-static DoneWith toDoneWith(DoneResult result)
-{
- return result == DoneResult::Success ? DoneWith::Success : DoneWith::Error;
-}
-
-class LoopThreadData
-{
- Q_DISABLE_COPY_MOVE(LoopThreadData)
-
-public:
- LoopThreadData() = default;
- void pushIteration(int index)
- {
- m_activeLoopStack.push_back(index);
- }
- void popIteration()
- {
- QT_ASSERT(m_activeLoopStack.size(), return);
- m_activeLoopStack.pop_back();
- }
- int iteration() const
- {
- QT_ASSERT(m_activeLoopStack.size(), qWarning(
- "The referenced loop is not reachable in the running tree. "
- "A -1 will be returned which might lead to a crash in the calling code. "
- "It is possible that no loop was added to the tree, "
- "or the loop is not reachable from where it is referenced."); return -1);
- return m_activeLoopStack.last();
- }
-
-private:
- QList<int> m_activeLoopStack;
-};
-
-class LoopData
-{
-public:
- LoopThreadData &threadData() {
- QMutexLocker lock(&m_threadDataMutex);
- return m_threadDataMap.try_emplace(QThread::currentThread()).first->second;
- }
-
- const std::optional<int> m_loopCount = {};
- const Loop::ValueGetter m_valueGetter = {};
- const Loop::Condition m_condition = {};
- QMutex m_threadDataMutex = {};
- // Use std::map on purpose, so that it doesn't invalidate references on modifications.
- // Don't optimize it by using std::unordered_map.
- std::map<QThread *, LoopThreadData> m_threadDataMap = {};
-};
-
-Loop::Loop()
- : m_loopData(new LoopData)
-{}
-
-Loop::Loop(int count, const ValueGetter &valueGetter)
- : m_loopData(new LoopData{count, valueGetter})
-{}
-
-Loop::Loop(const Condition &condition)
- : m_loopData(new LoopData{{}, {}, condition})
-{}
-
-int Loop::iteration() const
-{
- return m_loopData->threadData().iteration();
-}
-
-const void *Loop::valuePtr() const
-{
- return m_loopData->m_valueGetter(iteration());
-}
-
-using StoragePtr = void *;
-
-static constexpr QLatin1StringView s_activeStorageWarning =
- "The referenced storage is not reachable in the running tree. "
- "A nullptr will be returned which might lead to a crash in the calling code. "
- "It is possible that no storage was added to the tree, "
- "or the storage is not reachable from where it is referenced."_L1;
-
-class StorageThreadData
-{
- Q_DISABLE_COPY_MOVE(StorageThreadData)
-
-public:
- StorageThreadData() = default;
- void pushStorage(StoragePtr storagePtr)
- {
- m_activeStorageStack.push_back({storagePtr, activeTaskTree()});
- }
- void popStorage()
- {
- QT_ASSERT(m_activeStorageStack.size(), return);
- m_activeStorageStack.pop_back();
- }
- StoragePtr activeStorage() const
- {
- QT_ASSERT(m_activeStorageStack.size(),
- qWarning().noquote() << s_activeStorageWarning; return nullptr);
- const QPair<StoragePtr, TaskTree *> &top = m_activeStorageStack.last();
- QT_ASSERT(top.second == activeTaskTree(),
- qWarning().noquote() << s_activeStorageWarning; return nullptr);
- return top.first;
- }
-
-private:
- QList<QPair<StoragePtr, TaskTree *>> m_activeStorageStack;
-};
-
-class StorageData
-{
-public:
- StorageThreadData &threadData() {
- QMutexLocker lock(&m_threadDataMutex);
- return m_threadDataMap.try_emplace(QThread::currentThread()).first->second;
- }
-
- const StorageBase::StorageConstructor m_constructor = {};
- const StorageBase::StorageDestructor m_destructor = {};
- QMutex m_threadDataMutex = {};
- // Use std::map on purpose, so that it doesn't invalidate references on modifications.
- // Don't optimize it by using std::unordered_map.
- std::map<QThread *, StorageThreadData> m_threadDataMap = {};
-};
-
-StorageBase::StorageBase(const StorageConstructor &ctor, const StorageDestructor &dtor)
- : m_storageData(new StorageData{ctor, dtor})
-{}
-
-void *StorageBase::activeStorageVoid() const
-{
- return m_storageData->threadData().activeStorage();
-}
-
-void GroupItem::addChildren(const GroupItems &children)
-{
- QT_ASSERT(m_type == Type::Group || m_type == Type::List,
- qWarning("Only Group or List may have children, skipping..."); return);
- if (m_type == Type::List) {
- m_children.append(children);
- return;
- }
- for (const GroupItem &child : children) {
- switch (child.m_type) {
- case Type::List:
- addChildren(child.m_children);
- break;
- case Type::Group:
- m_children.append(child);
- break;
- case Type::GroupData:
- if (child.m_groupData.m_groupHandler.m_setupHandler) {
- QT_ASSERT(!m_groupData.m_groupHandler.m_setupHandler,
- qWarning("Group setup handler redefinition, overriding..."));
- m_groupData.m_groupHandler.m_setupHandler
- = child.m_groupData.m_groupHandler.m_setupHandler;
- }
- if (child.m_groupData.m_groupHandler.m_doneHandler) {
- QT_ASSERT(!m_groupData.m_groupHandler.m_doneHandler,
- qWarning("Group done handler redefinition, overriding..."));
- m_groupData.m_groupHandler.m_doneHandler
- = child.m_groupData.m_groupHandler.m_doneHandler;
- m_groupData.m_groupHandler.m_callDoneIf
- = child.m_groupData.m_groupHandler.m_callDoneIf;
- }
- if (child.m_groupData.m_parallelLimit) {
- QT_ASSERT(!m_groupData.m_parallelLimit,
- qWarning("Group execution mode redefinition, overriding..."));
- m_groupData.m_parallelLimit = child.m_groupData.m_parallelLimit;
- }
- if (child.m_groupData.m_workflowPolicy) {
- QT_ASSERT(!m_groupData.m_workflowPolicy,
- qWarning("Group workflow policy redefinition, overriding..."));
- m_groupData.m_workflowPolicy = child.m_groupData.m_workflowPolicy;
- }
- if (child.m_groupData.m_loop) {
- QT_ASSERT(!m_groupData.m_loop,
- qWarning("Group loop redefinition, overriding..."));
- m_groupData.m_loop = child.m_groupData.m_loop;
- }
- break;
- case Type::TaskHandler:
- QT_ASSERT(child.m_taskHandler.m_createHandler,
- qWarning("Task create handler can't be null, skipping..."); return);
- m_children.append(child);
- break;
- case Type::Storage:
- // Check for duplicates, as can't have the same storage twice on the same level.
- for (const StorageBase &storage : child.m_storageList) {
- if (m_storageList.contains(storage)) {
- QT_ASSERT(false, qWarning("Can't add the same storage into one Group twice, "
- "skipping..."));
- continue;
- }
- m_storageList.append(storage);
- }
- break;
- }
- }
-}
-
-/*!
- \class Tasking::ExecutableItem
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief Base class for executable task items.
- \reentrant
-
- \c ExecutableItem provides an additional interface for items containing executable tasks.
- Use withTimeout() to attach a timeout to a task.
- Use withLog() to include debugging information about the task startup and the execution result.
-*/
-
-/*!
- Attaches \c TimeoutTask to a copy of \c this ExecutableItem, elapsing after \a timeout
- in milliseconds, with an optionally provided timeout \a handler, and returns the coupled item.
-
- When the ExecutableItem finishes before \a timeout passes, the returned item finishes
- immediately with the task's result. Otherwise, \a handler is invoked (if provided),
- the task is canceled, and the returned item finishes with an error.
-*/
-Group ExecutableItem::withTimeout(milliseconds timeout,
- const std::function<void()> &handler) const
-{
- const auto onSetup = [timeout](milliseconds &timeoutData) { timeoutData = timeout; };
- return Group {
- parallel,
- stopOnSuccessOrError,
- Group {
- finishAllAndError,
- handler ? TimeoutTask(onSetup, [handler] { handler(); }, CallDoneIf::Success)
- : TimeoutTask(onSetup)
- },
- *this
- };
-}
-
-static QString currentTime() { return QTime::currentTime().toString(Qt::ISODateWithMs); }
-
-static QString logHeader(const QString &logName)
-{
- return QString::fromLatin1("TASK TREE LOG [%1] \"%2\"").arg(currentTime(), logName);
-};
-
-/*!
- Attaches a custom debug printout to a copy of \c this ExecutableItem,
- issued on task startup and after the task is finished, and returns the coupled item.
-
- The debug printout includes a timestamp of the event (start or finish)
- and \a logName to identify the specific task in the debug log.
-
- The finish printout contains the additional information whether the execution was
- synchronous or asynchronous, its result (the value described by the DoneWith enum),
- and the total execution time in milliseconds.
-*/
-Group ExecutableItem::withLog(const QString &logName) const
-{
- struct LogStorage
- {
- time_point<system_clock, nanoseconds> start;
- int asyncCount = 0;
- };
- const Storage<LogStorage> storage;
- return Group {
- storage,
- onGroupSetup([storage, logName] {
- storage->start = system_clock::now();
- storage->asyncCount = activeTaskTree()->asyncCount();
- qDebug().noquote().nospace() << logHeader(logName) << " started.";
- }),
- *this,
- onGroupDone([storage, logName](DoneWith result) {
- const auto elapsed = duration_cast<milliseconds>(system_clock::now() - storage->start);
- const int asyncCountDiff = activeTaskTree()->asyncCount() - storage->asyncCount;
- QT_CHECK(asyncCountDiff >= 0);
- const QMetaEnum doneWithEnum = QMetaEnum::fromType<DoneWith>();
- const QString syncType = asyncCountDiff ? QString::fromLatin1("asynchronously")
- : QString::fromLatin1("synchronously");
- qDebug().noquote().nospace() << logHeader(logName) << " finished " << syncType
- << " with " << doneWithEnum.valueToKey(int(result))
- << " within " << elapsed.count() << "ms.";
- })
- };
-}
-
-/*!
- \fn Group ExecutableItem::operator!(const ExecutableItem &item)
-
- Returns a Group with the DoneResult of \a item negated.
-
- If \a item reports DoneResult::Success, the returned item reports DoneResult::Error.
- If \a item reports DoneResult::Error, the returned item reports DoneResult::Success.
-
- The returned item is equivalent to:
- \code
- Group {
- item,
- onGroupDone([](DoneWith doneWith) { return toDoneResult(doneWith == DoneWith::Error); })
- }
- \endcode
-
- \sa operator&&(), operator||()
-*/
-Group operator!(const ExecutableItem &item)
-{
- return {
- item,
- onGroupDone([](DoneWith doneWith) { return toDoneResult(doneWith == DoneWith::Error); })
- };
-}
-
-/*!
- \fn Group ExecutableItem::operator&&(const ExecutableItem &first, const ExecutableItem &second)
-
- Returns a Group with \a first and \a second tasks merged with conjunction.
-
- Both \a first and \a second tasks execute in sequence.
- If both tasks report DoneResult::Success, the returned item reports DoneResult::Success.
- Otherwise, the returned item reports DoneResult::Error.
-
- The returned item is
- \l {https://en.wikipedia.org/wiki/Short-circuit_evaluation}{short-circuiting}:
- if the \a first task reports DoneResult::Error, the \a second task is skipped,
- and the returned item reports DoneResult::Error immediately.
-
- The returned item is equivalent to:
- \code
- Group { stopOnError, first, second }
- \endcode
-
- \note Parallel execution of conjunction in a short-circuit manner can be achieved with the
- following code: \c {Group { parallel, stopOnError, first, second }}. In this case:
- if the \e {first finished} task reports DoneResult::Error,
- the \e other task is canceled, and the group reports DoneResult::Error immediately.
-
- \sa operator||(), operator!()
-*/
-Group operator&&(const ExecutableItem &first, const ExecutableItem &second)
-{
- return { stopOnError, first, second };
-}
-
-/*!
- \fn Group ExecutableItem::operator||(const ExecutableItem &first, const ExecutableItem &second)
-
- Returns a Group with \a first and \a second tasks merged with disjunction.
-
- Both \a first and \a second tasks execute in sequence.
- If both tasks report DoneResult::Error, the returned item reports DoneResult::Error.
- Otherwise, the returned item reports DoneResult::Success.
-
- The returned item is
- \l {https://en.wikipedia.org/wiki/Short-circuit_evaluation}{short-circuiting}:
- if the \a first task reports DoneResult::Success, the \a second task is skipped,
- and the returned item reports DoneResult::Success immediately.
-
- The returned item is equivalent to:
- \code
- Group { stopOnSuccess, first, second }
- \endcode
-
- \note Parallel execution of disjunction in a short-circuit manner can be achieved with the
- following code: \c {Group { parallel, stopOnSuccess, first, second }}. In this case:
- if the \e {first finished} task reports DoneResult::Success,
- the \e other task is canceled, and the group reports DoneResult::Success immediately.
-
- \sa operator&&(), operator!()
-*/
-Group operator||(const ExecutableItem &first, const ExecutableItem &second)
-{
- return { stopOnSuccess, first, second };
-}
-
-/*!
- \fn Group ExecutableItem::operator&&(const ExecutableItem &item, DoneResult result)
- \overload ExecutableItem::operator&&()
-
- Returns the \a item task if the \a result is DoneResult::Success; otherwise returns
- the \a item task with its done result tweaked to DoneResult::Error.
-
- The \c {task && DoneResult::Error} is an eqivalent to tweaking the task's done result
- into DoneResult::Error unconditionally.
-*/
-Group operator&&(const ExecutableItem &item, DoneResult result)
-{
- return { result == DoneResult::Success ? stopOnError : finishAllAndError, item };
-}
-
-/*!
- \fn Group ExecutableItem::operator||(const ExecutableItem &item, DoneResult result)
- \overload ExecutableItem::operator||()
-
- Returns the \a item task if the \a result is DoneResult::Error; otherwise returns
- the \a item task with its done result tweaked to DoneResult::Success.
-
- The \c {task || DoneResult::Success} is an eqivalent to tweaking the task's done result
- into DoneResult::Success unconditionally.
-*/
-Group operator||(const ExecutableItem &item, DoneResult result)
-{
- return { result == DoneResult::Error ? stopOnError : finishAllAndSuccess, item };
-}
-
-Group ExecutableItem::withCancelImpl(
- const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper,
- const GroupItems &postCancelRecipe) const
-{
- const Storage<bool> canceledStorage(false);
-
- const auto onSetup = [connectWrapper, canceledStorage](Barrier &barrier) {
- connectWrapper(&barrier, [barrierPtr = &barrier, canceled = canceledStorage.activeStorage()] {
- *canceled = true;
- barrierPtr->advance();
- });
- };
-
- const auto wasCanceled = [canceledStorage] { return *canceledStorage; };
-
- return {
- continueOnError,
- canceledStorage,
- Group {
- parallel,
- stopOnSuccessOrError,
- BarrierTask(onSetup) && errorItem,
- *this
- },
- If (wasCanceled) >> Then {
- postCancelRecipe
- }
- };
-}
-
-Group ExecutableItem::withAcceptImpl(
- const std::function<void(QObject *, const std::function<void()> &)> &connectWrapper) const
-{
- const auto onSetup = [connectWrapper](Barrier &barrier) {
- connectWrapper(&barrier, [barrierPtr = &barrier] { barrierPtr->advance(); });
- };
- return Group {
- parallel,
- BarrierTask(onSetup),
- *this
- };
-}
-
-class TaskTreePrivate;
-class TaskNode;
-class RuntimeContainer;
-class RuntimeIteration;
-class RuntimeTask;
-
-class ExecutionContextActivator
-{
-public:
- ExecutionContextActivator(RuntimeIteration *iteration) {
- activateTaskTree(iteration);
- activateContext(iteration);
- }
- ExecutionContextActivator(RuntimeContainer *container) {
- activateTaskTree(container);
- activateContext(container);
- }
- ~ExecutionContextActivator() {
- for (int i = m_activeStorages.size() - 1; i >= 0; --i) // iterate in reverse order
- m_activeStorages[i].m_storageData->threadData().popStorage();
- for (int i = m_activeLoops.size() - 1; i >= 0; --i) // iterate in reverse order
- m_activeLoops[i].m_loopData->threadData().popIteration();
- QT_ASSERT(s_activeTaskTrees.size(), return);
- s_activeTaskTrees.pop_back();
- }
-
-private:
- void activateTaskTree(RuntimeIteration *iteration);
- void activateTaskTree(RuntimeContainer *container);
- void activateContext(RuntimeIteration *iteration);
- void activateContext(RuntimeContainer *container);
- QList<Loop> m_activeLoops;
- QList<StorageBase> m_activeStorages;
-};
-
-class ContainerNode
-{
- Q_DISABLE_COPY(ContainerNode)
-
-public:
- ContainerNode(ContainerNode &&other) = default;
- ContainerNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task);
-
- TaskTreePrivate *const m_taskTreePrivate = nullptr;
-
- const GroupItem::GroupHandler m_groupHandler;
- const int m_parallelLimit = 1;
- const WorkflowPolicy m_workflowPolicy = WorkflowPolicy::StopOnError;
- const std::optional<Loop> m_loop;
- const QList<StorageBase> m_storageList;
- std::vector<TaskNode> m_children;
- const int m_taskCount = 0;
-};
-
-class TaskNode
-{
- Q_DISABLE_COPY(TaskNode)
-
-public:
- TaskNode(TaskNode &&other) = default;
- TaskNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task)
- : m_taskHandler(task.m_taskHandler)
- , m_container(taskTreePrivate, task)
- {}
-
- bool isTask() const { return bool(m_taskHandler.m_createHandler); }
- int taskCount() const { return isTask() ? 1 : m_container.m_taskCount; }
-
- const GroupItem::TaskHandler m_taskHandler;
- ContainerNode m_container;
-};
-
-class TaskTreePrivate
-{
- Q_DISABLE_COPY_MOVE(TaskTreePrivate)
-
-public:
- explicit TaskTreePrivate(TaskTree *taskTree);
- ~TaskTreePrivate();
-
- void start();
- void stop();
- void bumpAsyncCount();
- void advanceProgress(int byValue);
- void emitDone(DoneWith result);
- void callSetupHandler(const StorageBase &storage, StoragePtr storagePtr) {
- callStorageHandler(storage, storagePtr, &StorageHandler::m_setupHandler);
- }
- void callDoneHandler(const StorageBase &storage, StoragePtr storagePtr) {
- callStorageHandler(storage, storagePtr, &StorageHandler::m_doneHandler);
- }
- struct StorageHandler {
- StorageBase::StorageHandler m_setupHandler = {};
- StorageBase::StorageHandler m_doneHandler = {};
- };
- typedef StorageBase::StorageHandler StorageHandler::*HandlerPtr; // ptr to class member
- void callStorageHandler(const StorageBase &storage, StoragePtr storagePtr, HandlerPtr ptr)
- {
- const auto it = m_storageHandlers.constFind(storage);
- if (it == m_storageHandlers.constEnd())
- return;
- const StorageHandler storageHandler = *it;
- if (storageHandler.*ptr) {
- GuardLocker locker(m_guard);
- (storageHandler.*ptr)(storagePtr);
- }
- }
-
- // Node related methods
-
- // If returned value != Continue, childDone() needs to be called in parent container (in caller)
- // in order to unwind properly.
- void startTask(const std::shared_ptr<RuntimeTask> &node);
- void stopTask(RuntimeTask *node);
- bool invokeTaskDoneHandler(RuntimeTask *node, DoneWith doneWith);
-
- // Container related methods
-
- void continueContainer(RuntimeContainer *container);
- void startChildren(RuntimeContainer *container);
- void childDone(RuntimeIteration *iteration, bool success);
- void stopContainer(RuntimeContainer *container);
- bool invokeDoneHandler(RuntimeContainer *container, DoneWith doneWith);
- bool invokeLoopHandler(RuntimeContainer *container);
-
- template <typename Container, typename Handler, typename ...Args,
- typename ReturnType = std::invoke_result_t<Handler, Args...>>
- ReturnType invokeHandler(Container *container, Handler &&handler, Args &&...args)
- {
- QT_ASSERT(!m_guard.isLocked(), qWarning("Nested execution of handlers detected."
- "This may happen when one task's handler has entered a nested event loop,"
- "and other task finished during nested event loop's processing, "
- "causing stopping (canceling) the task executing the nested event loop. "
- "This includes the case when QCoreApplication::processEvents() was called from "
- "the handler. It may also happen when the Barrier task is advanced directly "
- "from some other task handler. This will lead to a crash. "
- "Avoid event processing during handlers' execution. "
- "If it can't be avoided, make sure no other tasks are run in parallel when "
- "processing events from the handler."));
- ExecutionContextActivator activator(container);
- GuardLocker locker(m_guard);
- return std::invoke(std::forward<Handler>(handler), std::forward<Args>(args)...);
- }
-
- static int effectiveLoopCount(const std::optional<Loop> &loop)
- {
- return loop && loop->m_loopData->m_loopCount ? *loop->m_loopData->m_loopCount : 1;
- }
-
- TaskTree *q = nullptr;
- Guard m_guard;
- int m_progressValue = 0;
- int m_asyncCount = 0;
- QSet<StorageBase> m_storages;
- QHash<StorageBase, StorageHandler> m_storageHandlers;
- std::optional<TaskNode> m_root;
- std::shared_ptr<RuntimeTask> m_runtimeRoot; // Keep me last in order to destruct first
-};
-
-static bool initialSuccessBit(WorkflowPolicy workflowPolicy)
-{
- switch (workflowPolicy) {
- case WorkflowPolicy::StopOnError:
- case WorkflowPolicy::ContinueOnError:
- case WorkflowPolicy::FinishAllAndSuccess:
- return true;
- case WorkflowPolicy::StopOnSuccess:
- case WorkflowPolicy::ContinueOnSuccess:
- case WorkflowPolicy::StopOnSuccessOrError:
- case WorkflowPolicy::FinishAllAndError:
- return false;
- }
- QT_CHECK(false);
- return false;
-}
-
-static bool isProgressive(RuntimeContainer *container);
-
-class RuntimeIteration
-{
- Q_DISABLE_COPY(RuntimeIteration)
-
-public:
- RuntimeIteration(int index, RuntimeContainer *container);
- ~RuntimeIteration();
- std::optional<Loop> loop() const;
- void removeChild(RuntimeTask *node);
-
- const int m_iterationIndex = 0;
- const bool m_isProgressive = true;
- RuntimeContainer *m_container = nullptr;
- int m_doneCount = 0;
- std::vector<std::shared_ptr<RuntimeTask>> m_children = {}; // Owning.
-};
-
-class RuntimeContainer
-{
- Q_DISABLE_COPY(RuntimeContainer)
-
-public:
- RuntimeContainer(const ContainerNode &taskContainer, RuntimeTask *parentTask)
- : m_containerNode(taskContainer)
- , m_parentTask(parentTask)
- , m_storages(createStorages(taskContainer))
- , m_successBit(initialSuccessBit(taskContainer.m_workflowPolicy))
- , m_shouldIterate(taskContainer.m_loop)
- {}
-
- ~RuntimeContainer()
- {
- for (int i = m_containerNode.m_storageList.size() - 1; i >= 0; --i) { // iterate in reverse order
- const StorageBase storage = m_containerNode.m_storageList[i];
- StoragePtr storagePtr = m_storages.value(i);
- if (m_callStorageDoneHandlersOnDestruction)
- m_containerNode.m_taskTreePrivate->callDoneHandler(storage, storagePtr);
- storage.m_storageData->m_destructor(storagePtr);
- }
- }
-
- static QList<StoragePtr> createStorages(const ContainerNode &container);
- bool isStarting() const { return m_startGuard.isLocked(); }
- RuntimeIteration *parentIteration() const;
- bool updateSuccessBit(bool success);
- void deleteFinishedIterations();
- int progressiveLoopCount() const
- {
- return m_containerNode.m_taskTreePrivate->effectiveLoopCount(m_containerNode.m_loop);
- }
-
- const ContainerNode &m_containerNode; // Not owning.
- RuntimeTask *m_parentTask = nullptr; // Not owning.
- const QList<StoragePtr> m_storages; // Owning.
-
- bool m_successBit = true;
- bool m_callStorageDoneHandlersOnDestruction = false;
- Guard m_startGuard;
-
- int m_iterationCount = 0;
- int m_nextToStart = 0;
- int m_runningChildren = 0;
- bool m_shouldIterate = true;
- std::vector<std::unique_ptr<RuntimeIteration>> m_iterations; // Owning.
-};
-
-class RuntimeTask
-{
-public:
- ~RuntimeTask()
- {
- if (m_task) {
- // Ensures the running task's d'tor doesn't emit done() signal. QTCREATORBUG-30204.
- QObject::disconnect(m_task.get(), &TaskInterface::done, nullptr, nullptr);
- }
- }
-
- const TaskNode &m_taskNode; // Not owning.
- RuntimeIteration *m_parentIteration = nullptr; // Not owning.
- std::optional<RuntimeContainer> m_container = {}; // Owning.
- std::unique_ptr<TaskInterface> m_task = {}; // Owning.
- SetupResult m_setupResult = SetupResult::Continue;
-};
-
-RuntimeIteration::~RuntimeIteration() = default;
-
-TaskTreePrivate::TaskTreePrivate(TaskTree *taskTree)
- : q(taskTree) {}
-
-TaskTreePrivate::~TaskTreePrivate() = default;
-
-static bool isProgressive(RuntimeContainer *container)
-{
- RuntimeIteration *iteration = container->m_parentTask->m_parentIteration;
- return iteration ? iteration->m_isProgressive : true;
-}
-
-void ExecutionContextActivator::activateTaskTree(RuntimeIteration *iteration)
-{
- activateTaskTree(iteration->m_container);
-}
-
-void ExecutionContextActivator::activateTaskTree(RuntimeContainer *container)
-{
- s_activeTaskTrees.push_back(container->m_containerNode.m_taskTreePrivate->q);
-}
-
-void ExecutionContextActivator::activateContext(RuntimeIteration *iteration)
-{
- std::optional<Loop> loop = iteration->loop();
- if (loop) {
- loop->m_loopData->threadData().pushIteration(iteration->m_iterationIndex);
- m_activeLoops.append(*loop);
- }
- activateContext(iteration->m_container);
-}
-
-void ExecutionContextActivator::activateContext(RuntimeContainer *container)
-{
- const ContainerNode &containerNode = container->m_containerNode;
- for (int i = 0; i < containerNode.m_storageList.size(); ++i) {
- const StorageBase &storage = containerNode.m_storageList[i];
- if (m_activeStorages.contains(storage))
- continue; // Storage shadowing: The storage is already active, skipping it...
- m_activeStorages.append(storage);
- storage.m_storageData->threadData().pushStorage(container->m_storages.value(i));
- }
- // Go to the parent after activating this storages so that storage shadowing works
- // in the direction from child to parent root.
- if (container->parentIteration())
- activateContext(container->parentIteration());
-}
-
-void TaskTreePrivate::start()
-{
- QT_ASSERT(m_root, return);
- QT_ASSERT(!m_runtimeRoot, return);
- m_asyncCount = 0;
- m_progressValue = 0;
- {
- GuardLocker locker(m_guard);
- emit q->started();
- emit q->asyncCountChanged(m_asyncCount);
- emit q->progressValueChanged(m_progressValue);
- }
- // TODO: check storage handlers for not existing storages in tree
- for (auto it = m_storageHandlers.cbegin(); it != m_storageHandlers.cend(); ++it) {
- QT_ASSERT(m_storages.contains(it.key()), qWarning("The registered storage doesn't "
- "exist in task tree. Its handlers will never be called."));
- }
- m_runtimeRoot.reset(new RuntimeTask{*m_root});
- startTask(m_runtimeRoot);
- bumpAsyncCount();
-}
-
-void TaskTreePrivate::stop()
-{
- QT_ASSERT(m_root, return);
- if (!m_runtimeRoot)
- return;
- stopTask(m_runtimeRoot.get());
- m_runtimeRoot.reset();
- emitDone(DoneWith::Cancel);
-}
-
-void TaskTreePrivate::bumpAsyncCount()
-{
- if (!m_runtimeRoot)
- return;
- ++m_asyncCount;
- GuardLocker locker(m_guard);
- emit q->asyncCountChanged(m_asyncCount);
-}
-
-void TaskTreePrivate::advanceProgress(int byValue)
-{
- if (byValue == 0)
- return;
- QT_CHECK(byValue > 0);
- QT_CHECK(m_progressValue + byValue <= m_root->taskCount());
- m_progressValue += byValue;
- GuardLocker locker(m_guard);
- emit q->progressValueChanged(m_progressValue);
-}
-
-void TaskTreePrivate::emitDone(DoneWith result)
-{
- QT_CHECK(m_progressValue == m_root->taskCount());
- GuardLocker locker(m_guard);
- emit q->done(result);
-}
-
-RuntimeIteration::RuntimeIteration(int index, RuntimeContainer *container)
- : m_iterationIndex(index)
- , m_isProgressive(index < container->progressiveLoopCount() && isProgressive(container))
- , m_container(container)
-{}
-
-std::optional<Loop> RuntimeIteration::loop() const
-{
- return m_container->m_containerNode.m_loop;
-}
-
-void RuntimeIteration::removeChild(RuntimeTask *task)
-{
- const auto it = std::find_if(m_children.cbegin(), m_children.cend(), [task](const auto &ptr) {
- return ptr.get() == task;
- });
- if (it != m_children.cend())
- m_children.erase(it);
-}
-
-static std::vector<TaskNode> createChildren(TaskTreePrivate *taskTreePrivate,
- const GroupItems &children)
-{
- std::vector<TaskNode> result;
- result.reserve(children.size());
- for (const GroupItem &child : children)
- result.emplace_back(taskTreePrivate, child);
- return result;
-}
-
-ContainerNode::ContainerNode(TaskTreePrivate *taskTreePrivate, const GroupItem &task)
- : m_taskTreePrivate(taskTreePrivate)
- , m_groupHandler(task.m_groupData.m_groupHandler)
- , m_parallelLimit(task.m_groupData.m_parallelLimit.value_or(1))
- , m_workflowPolicy(task.m_groupData.m_workflowPolicy.value_or(WorkflowPolicy::StopOnError))
- , m_loop(task.m_groupData.m_loop)
- , m_storageList(task.m_storageList)
- , m_children(createChildren(taskTreePrivate, task.m_children))
- , m_taskCount(std::accumulate(m_children.cbegin(), m_children.cend(), 0,
- [](int r, const TaskNode &n) { return r + n.taskCount(); })
- * taskTreePrivate->effectiveLoopCount(m_loop))
-{
- for (const StorageBase &storage : m_storageList)
- m_taskTreePrivate->m_storages << storage;
-}
-
-QList<StoragePtr> RuntimeContainer::createStorages(const ContainerNode &container)
-{
- QList<StoragePtr> storages;
- for (const StorageBase &storage : container.m_storageList) {
- StoragePtr storagePtr = storage.m_storageData->m_constructor();
- storages.append(storagePtr);
- container.m_taskTreePrivate->callSetupHandler(storage, storagePtr);
- }
- return storages;
-}
-
-RuntimeIteration *RuntimeContainer::parentIteration() const
-{
- return m_parentTask->m_parentIteration;
-}
-
-bool RuntimeContainer::updateSuccessBit(bool success)
-{
- if (m_containerNode.m_workflowPolicy == WorkflowPolicy::FinishAllAndSuccess
- || m_containerNode.m_workflowPolicy == WorkflowPolicy::FinishAllAndError
- || m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccessOrError) {
- if (m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccessOrError)
- m_successBit = success;
- return m_successBit;
- }
-
- const bool donePolicy = m_containerNode.m_workflowPolicy == WorkflowPolicy::StopOnSuccess
- || m_containerNode.m_workflowPolicy == WorkflowPolicy::ContinueOnSuccess;
- m_successBit = donePolicy ? (m_successBit || success) : (m_successBit && success);
- return m_successBit;
-}
-
-void RuntimeContainer::deleteFinishedIterations()
-{
- for (auto it = m_iterations.cbegin(); it != m_iterations.cend(); ) {
- if (it->get()->m_doneCount == int(m_containerNode.m_children.size()))
- it = m_iterations.erase(it);
- else
- ++it;
- }
-}
-
-void TaskTreePrivate::continueContainer(RuntimeContainer *container)
-{
- RuntimeTask *parentTask = container->m_parentTask;
- if (parentTask->m_setupResult == SetupResult::Continue)
- startChildren(container);
- if (parentTask->m_setupResult == SetupResult::Continue)
- return;
-
- const bool bit = container->updateSuccessBit(parentTask->m_setupResult == SetupResult::StopWithSuccess);
- RuntimeIteration *parentIteration = container->parentIteration();
- QT_CHECK(parentTask);
- const bool result = invokeDoneHandler(container, bit ? DoneWith::Success : DoneWith::Error);
- parentTask->m_setupResult = toSetupResult(result);
- if (parentIteration) {
- parentIteration->removeChild(parentTask);
- if (!parentIteration->m_container->isStarting())
- childDone(parentIteration, result);
- } else {
- QT_CHECK(m_runtimeRoot.get() == parentTask);
- m_runtimeRoot.reset();
- emitDone(result ? DoneWith::Success : DoneWith::Error);
- }
-}
-
-void TaskTreePrivate::startChildren(RuntimeContainer *container)
-{
- const ContainerNode &containerNode = container->m_containerNode;
- const int childCount = int(containerNode.m_children.size());
-
- if (container->m_iterationCount == 0) {
- if (container->m_shouldIterate && !invokeLoopHandler(container)) {
- if (isProgressive(container))
- advanceProgress(containerNode.m_taskCount);
- container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit);
- return;
- }
- container->m_iterations.emplace_back(
- std::make_unique<RuntimeIteration>(container->m_iterationCount, container));
- ++container->m_iterationCount;
- }
-
- GuardLocker locker(container->m_startGuard);
-
- while (containerNode.m_parallelLimit == 0
- || container->m_runningChildren < containerNode.m_parallelLimit) {
- container->deleteFinishedIterations();
- if (container->m_nextToStart == childCount) {
- if (invokeLoopHandler(container)) {
- container->m_nextToStart = 0;
- container->m_iterations.emplace_back(
- std::make_unique<RuntimeIteration>(container->m_iterationCount, container));
- ++container->m_iterationCount;
- } else if (container->m_iterations.empty()) {
- container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit);
- return;
- } else {
- return;
- }
- }
- if (containerNode.m_children.size() == 0) // Empty loop body.
- continue;
-
- RuntimeIteration *iteration = container->m_iterations.back().get();
- const std::shared_ptr<RuntimeTask> task(
- new RuntimeTask{containerNode.m_children.at(container->m_nextToStart), iteration});
- iteration->m_children.emplace_back(task);
- ++container->m_runningChildren;
- ++container->m_nextToStart;
-
- startTask(task);
- if (task->m_setupResult == SetupResult::Continue)
- continue;
-
- task->m_parentIteration->removeChild(task.get());
- childDone(iteration, task->m_setupResult == SetupResult::StopWithSuccess);
- if (container->m_parentTask->m_setupResult != SetupResult::Continue)
- return;
- }
-}
-
-void TaskTreePrivate::childDone(RuntimeIteration *iteration, bool success)
-{
- RuntimeContainer *container = iteration->m_container;
- const WorkflowPolicy &workflowPolicy = container->m_containerNode.m_workflowPolicy;
- const bool shouldStop = workflowPolicy == WorkflowPolicy::StopOnSuccessOrError
- || (workflowPolicy == WorkflowPolicy::StopOnSuccess && success)
- || (workflowPolicy == WorkflowPolicy::StopOnError && !success);
- ++iteration->m_doneCount;
- --container->m_runningChildren;
- const bool updatedSuccess = container->updateSuccessBit(success);
- container->m_parentTask->m_setupResult = shouldStop ? toSetupResult(updatedSuccess) : SetupResult::Continue;
- if (shouldStop)
- stopContainer(container);
-
- if (container->isStarting())
- return;
- continueContainer(container);
-}
-
-void TaskTreePrivate::stopContainer(RuntimeContainer *container)
-{
- const ContainerNode &containerNode = container->m_containerNode;
- for (auto &iteration : container->m_iterations) {
- for (auto &child : iteration->m_children) {
- ++iteration->m_doneCount;
- stopTask(child.get());
- }
-
- if (iteration->m_isProgressive) {
- int skippedTaskCount = 0;
- for (int i = iteration->m_doneCount; i < int(containerNode.m_children.size()); ++i)
- skippedTaskCount += containerNode.m_children.at(i).taskCount();
- advanceProgress(skippedTaskCount);
- }
- }
- const int skippedIterations = container->progressiveLoopCount() - container->m_iterationCount;
- if (skippedIterations > 0) {
- advanceProgress(container->m_containerNode.m_taskCount / container->progressiveLoopCount()
- * skippedIterations);
- }
-}
-
-static bool shouldCall(CallDoneIf callDoneIf, DoneWith result)
-{
- if (result == DoneWith::Success)
- return callDoneIf != CallDoneIf::Error;
- return callDoneIf != CallDoneIf::Success;
-}
-
-bool TaskTreePrivate::invokeDoneHandler(RuntimeContainer *container, DoneWith doneWith)
-{
- DoneResult result = toDoneResult(doneWith);
- const GroupItem::GroupHandler &groupHandler = container->m_containerNode.m_groupHandler;
- if (groupHandler.m_doneHandler && shouldCall(groupHandler.m_callDoneIf, doneWith))
- result = invokeHandler(container, groupHandler.m_doneHandler, doneWith);
- container->m_callStorageDoneHandlersOnDestruction = true;
- return result == DoneResult::Success;
-}
-
-bool TaskTreePrivate::invokeLoopHandler(RuntimeContainer *container)
-{
- if (container->m_shouldIterate) {
- const LoopData *loopData = container->m_containerNode.m_loop->m_loopData.get();
- if (loopData->m_loopCount) {
- container->m_shouldIterate = container->m_iterationCount < loopData->m_loopCount;
- } else if (loopData->m_condition) {
- container->m_shouldIterate = invokeHandler(container, loopData->m_condition,
- container->m_iterationCount);
- }
- }
- return container->m_shouldIterate;
-}
-
-void TaskTreePrivate::startTask(const std::shared_ptr<RuntimeTask> &node)
-{
- if (!node->m_taskNode.isTask()) {
- const ContainerNode &containerNode = node->m_taskNode.m_container;
- node->m_container.emplace(containerNode, node.get());
- RuntimeContainer *container = &*node->m_container;
- if (containerNode.m_groupHandler.m_setupHandler) {
- container->m_parentTask->m_setupResult = invokeHandler(container, containerNode.m_groupHandler.m_setupHandler);
- if (container->m_parentTask->m_setupResult != SetupResult::Continue) {
- if (isProgressive(container))
- advanceProgress(containerNode.m_taskCount);
- // Non-Continue SetupResult takes precedence over the workflow policy.
- container->m_successBit = container->m_parentTask->m_setupResult == SetupResult::StopWithSuccess;
- }
- }
- continueContainer(container);
- return;
- }
-
- const GroupItem::TaskHandler &handler = node->m_taskNode.m_taskHandler;
- node->m_task.reset(handler.m_createHandler());
- node->m_setupResult = handler.m_setupHandler
- ? invokeHandler(node->m_parentIteration, handler.m_setupHandler, *node->m_task.get())
- : SetupResult::Continue;
- if (node->m_setupResult != SetupResult::Continue) {
- if (node->m_parentIteration->m_isProgressive)
- advanceProgress(1);
- node->m_parentIteration->removeChild(node.get());
- return;
- }
- QObject::connect(node->m_task.get(), &TaskInterface::done,
- q, [this, node](DoneResult doneResult) {
- const bool result = invokeTaskDoneHandler(node.get(), toDoneWith(doneResult));
- node->m_setupResult = toSetupResult(result);
- QObject::disconnect(node->m_task.get(), &TaskInterface::done, q, nullptr);
- node->m_task.release()->deleteLater();
- RuntimeIteration *parentIteration = node->m_parentIteration;
- if (parentIteration->m_container->isStarting())
- return;
-
- parentIteration->removeChild(node.get());
- childDone(parentIteration, result);
- bumpAsyncCount();
- });
- node->m_task->start();
-}
-
-void TaskTreePrivate::stopTask(RuntimeTask *node)
-{
- if (!node->m_task) {
- if (!node->m_container)
- return;
- stopContainer(&*node->m_container);
- node->m_container->updateSuccessBit(false);
- invokeDoneHandler(&*node->m_container, DoneWith::Cancel);
- return;
- }
-
- invokeTaskDoneHandler(node, DoneWith::Cancel);
- node->m_task.reset();
-}
-
-bool TaskTreePrivate::invokeTaskDoneHandler(RuntimeTask *node, DoneWith doneWith)
-{
- DoneResult result = toDoneResult(doneWith);
- const GroupItem::TaskHandler &handler = node->m_taskNode.m_taskHandler;
- if (handler.m_doneHandler && shouldCall(handler.m_callDoneIf, doneWith)) {
- result = invokeHandler(node->m_parentIteration,
- handler.m_doneHandler, *node->m_task.get(), doneWith);
- }
- if (node->m_parentIteration->m_isProgressive)
- advanceProgress(1);
- return result == DoneResult::Success;
-}
-
-/*!
- \class Tasking::TaskTree
- \inheaderfile solutions/tasking/tasktree.h
- \inmodule TaskingSolution
- \brief The TaskTree class runs an async task tree structure defined in a declarative way.
- \reentrant
-
- Use the Tasking namespace to build extensible, declarative task tree
- structures that contain possibly asynchronous tasks, such as QProcess,
- NetworkQuery, or ConcurrentCall<ReturnType>. TaskTree structures enable you
- to create a sophisticated mixture of a parallel or sequential flow of tasks
- in the form of a tree and to run it any time later.
-
- \section1 Root Element and Tasks
-
- The TaskTree has a mandatory Group root element, which may contain
- any number of tasks of various types, such as QProcessTask, NetworkQueryTask,
- or ConcurrentCallTask<ReturnType>:
-
- \code
- using namespace Tasking;
-
- const Group root {
- QProcessTask(...),
- NetworkQueryTask(...),
- ConcurrentCallTask<int>(...)
- };
-
- TaskTree *taskTree = new TaskTree(root);
- connect(taskTree, &TaskTree::done, ...); // finish handler
- taskTree->start();
- \endcode
-
- The task tree above has a top level element of the Group type that contains
- tasks of the QProcessTask, NetworkQueryTask, and ConcurrentCallTask<int> type.
- After taskTree->start() is called, the tasks are run in a chain, starting
- with QProcessTask. When the QProcessTask finishes successfully, the NetworkQueryTask
- task is started. Finally, when the network task finishes successfully, the
- ConcurrentCallTask<int> task is started.
-
- When the last running task finishes with success, the task tree is considered
- to have run successfully and the 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 TaskTree::done() signal with DoneWith::Error.
-
- \section1 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:
-
- \code
- const Group root {
- Group {
- parallel,
- QProcessTask(...),
- ConcurrentCallTask<int>(...)
- },
- NetworkQueryTask(...)
- };
- \endcode
-
- The example above differs from the first example in that the root element has
- a subgroup that contains the QProcessTask and ConcurrentCallTask<int>. The subgroup is a
- sibling element of the NetworkQueryTask in the root. The subgroup contains an
- additional \e parallel element that instructs its Group to execute its tasks
- in parallel.
-
- So, when the tree above is started, the QProcessTask and ConcurrentCallTask<int> start
- immediately and run in parallel. Since the root group doesn't contain a
- \e parallel element, its direct child tasks are run in sequence. Thus, the
- NetworkQueryTask 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 (QProcessTask or ConcurrentCallTask<int>), the
- following scenarios can take place:
-
- \table
- \header
- \li Scenario 1
- \li Scenario 2
- \row
- \li Root Group starts
- \li Root Group starts
- \row
- \li Sub Group starts
- \li Sub Group starts
- \row
- \li QProcessTask starts
- \li QProcessTask starts
- \row
- \li ConcurrentCallTask<int> starts
- \li ConcurrentCallTask<int> starts
- \row
- \li ...
- \li ...
- \row
- \li \b {QProcessTask finishes}
- \li \b {ConcurrentCallTask<int> finishes}
- \row
- \li ...
- \li ...
- \row
- \li \b {ConcurrentCallTask<int> finishes}
- \li \b {QProcessTask finishes}
- \row
- \li Sub Group finishes
- \li Sub Group finishes
- \row
- \li NetworkQueryTask starts
- \li NetworkQueryTask starts
- \row
- \li ...
- \li ...
- \row
- \li NetworkQueryTask finishes
- \li NetworkQueryTask finishes
- \row
- \li Root Group finishes
- \li Root Group finishes
- \endtable
-
- 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 QProcessTask finishes with an error while ConcurrentCallTask<int> is still being executed,
- the ConcurrentCallTask<int> is automatically canceled, the subgroup finishes with an error,
- the NetworkQueryTask is skipped, and the tree finishes with an error.
-
- \section1 Task Types
-
- Each task type is associated with its corresponding task class that executes
- the task. For example, a QProcessTask inside a task tree is associated with
- the QProcess class that executes the process. The associated objects are
- automatically created, started, and destructed exclusively by the task tree
- at the appropriate time.
-
- If a root group consists of five sequential QProcessTask tasks, and the task tree
- executes the group, it creates an instance of QProcess for the first
- QProcessTask and starts it. If the QProcess instance finishes successfully,
- the task tree destructs it and creates a new QProcess instance for the
- second QProcessTask, and so on. If the first task finishes with an error, the task
- tree stops creating QProcess instances, and the root group finishes with an
- error.
-
- The following table shows examples of task types and their corresponding task
- classes:
-
- \table
- \header
- \li Task Type (Tasking Namespace)
- \li Associated Task Class
- \li Brief Description
- \row
- \li QProcessTask
- \li QProcess
- \li Starts process.
- \row
- \li ConcurrentCallTask<ReturnType>
- \li Tasking::ConcurrentCall<ReturnType>
- \li Starts asynchronous task, runs in separate thread.
- \row
- \li TaskTreeTask
- \li Tasking::TaskTree
- \li Starts nested task tree.
- \row
- \li NetworkQueryTask
- \li NetworkQuery
- \li Starts network download.
- \endtable
-
- \section1 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.
-
- \section2 Task's Start Handler
-
- When a corresponding task class 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 \e reference to the associated task class object:
-
- \code
- const auto onSetup = [](QProcess &process) {
- process.setProgram("sleep");
- process.setArguments({"3"});
- };
- const Group root {
- QProcessTask(onSetup)
- };
- \endcode
-
- 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 \c {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:
-
- \table
- \header
- \li SetupResult Value
- \li Brief Description
- \row
- \li Continue
- \li 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).
- \row
- \li StopWithSuccess
- \li The task won't be started and it will report success to its parent.
- \row
- \li StopWithError
- \li The task won't be started and it will report an error to its parent.
- \endtable
-
- 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 \l Storage.
-
- \section2 Task's Done Handler
-
- When a running task finishes, the task tree invokes an optionally provided done handler.
- The handler should take a \c const \e reference to the associated task class object:
-
- \code
- 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)
- };
- \endcode
-
- 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.
-
- \section1 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.
-
- \section2 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:
-
- \code
- const auto onSetup = [] {
- qDebug() << "Entering the group";
- };
- const Group root {
- onGroupSetup(onSetup),
- QProcessTask(...)
- };
- \endcode
-
- 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 SetupResult::Continue, so that all
- tasks are started normally. Otherwise, when the start handler returns
- SetupResult::StopWithSuccess or SetupResult::StopWithError, the tasks are not
- started (they are skipped) and the group itself reports success or failure,
- depending on the returned value, respectively.
-
- \code
- 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
- };
- \endcode
-
- 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:
-
- \table
- \header
- \li Scenario
- \li Comment
- \row
- \li Root Group starts
- \li Doesn't return SetupResult, so its tasks are executed.
- \row
- \li Group 1 starts
- \li Returns Continue, so its tasks are executed.
- \row
- \li Process 1 starts
- \li
- \row
- \li ...
- \li ...
- \row
- \li Process 1 finishes (success)
- \li
- \row
- \li Group 1 finishes (success)
- \li
- \row
- \li Group 2 starts
- \li Returns StopWithSuccess, so Process 2 is skipped and Group 2 reports
- success.
- \row
- \li Group 2 finishes (success)
- \li
- \row
- \li Group 3 starts
- \li Returns StopWithError, so Process 3 is skipped and Group 3 reports
- an error.
- \row
- \li Group 3 finishes (error)
- \li
- \row
- \li Root Group finishes (error)
- \li 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.
- \endtable
-
- \section2 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 \l {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:
-
- \code
- 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";
- })
- };
- \endcode
-
- 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.
-
- \section1 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.
-
- \section2 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 \l sequential and
- \l 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.
-
- \section2 Workflow Policy
-
- The workflow policy element in a Group specifies how the group should behave
- when any of its \e 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.
-
- \section2 Storage
-
- Use the \l {Tasking::Storage} {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:
-
- \code
- 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](ConcurrentCall<QByteArray> &async) {
- async.setConcurrentCallData(&load, source);
- };
- // [4] runtime: task tree activates the instance from [7] before invoking handler
- const auto onLoaderDone = [storage](const ConcurrentCall<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](ConcurrentCall<void> &async) {
- const QByteArray content = storage->content; // [6] saver takes data from storage
- async.setConcurrentCallData(&save, destination, content);
- };
- const auto onSaverDone = [](const ConcurrentCall<void> &async) {
- qDebug() << "Save done successfully";
- };
-
- const Group root {
- // [7] runtime: task tree creates an instance of CopyStorage when root is entered
- storage,
- ConcurrentCallTask<QByteArray>(onLoaderSetup, onLoaderDone, CallDoneIf::Success),
- ConcurrentCallTask<void>(onSaverSetup, onSaverDone, CallDoneIf::Success)
- };
- return root;
- }
-
- const QString source = ...;
- const QString destination = ...;
- TaskTree taskTree(copyRecipe(source, destination));
- connect(&taskTree, &TaskTree::done,
- &taskTree, [](DoneWith result) {
- if (result == DoneWith::Success)
- qDebug() << "The copying finished successfully.";
- });
- tasktree.start();
- \endcode
-
- In the example above, the inter-task data consists of a QByteArray content
- variable [2] enclosed in a \c CopyStorage custom struct [1]. If the loader
- finishes successfully, it stores the data in a \c CopyStorage::content
- variable [5]. The saver then uses the variable to configure the saving task [6].
-
- To enable a task tree to manage the \c CopyStorage struct, an instance of
- \l {Tasking::Storage} {Storage}<\c CopyStorage> is created [3]. If a copy of this object is
- inserted as the group's child item [7], an instance of the \c CopyStorage struct is
- created dynamically when the task tree enters this group. When the task
- tree leaves this group, the existing instance of the \c CopyStorage struct is
- destructed as it's no longer needed.
-
- If several task trees holding a copy of the common
- \l {Tasking::Storage} {Storage}<\c 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 \c CopyStorage struct.
-
- You can access \c 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 \l {Tasking::Storage} {Storage}<\c 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 \c CopyStorage instance inside the
- \l {Tasking::Storage} {Storage}<\c CopyStorage> object. Therefore, the \c CopyStorage struct
- may be accessed only from within the handler body. To access the currently active
- \c CopyStorage from within \l {Tasking::Storage} {Storage}<\c CopyStorage>, use the
- \l {Tasking::Storage::operator->()} {Storage::operator->()},
- \l {Tasking::Storage::operator*()} {Storage::operator*()}, or Storage::activeStorage() method.
-
- The following list summarizes how to employ a Storage object into the task
- tree:
- \list 1
- \li Define the custom structure \c MyStorage with custom data [1], [2]
- \li Create an instance of the \l {Tasking::Storage} {Storage}<\c MyStorage> storage [3]
- \li Pass the \l {Tasking::Storage} {Storage}<\c MyStorage> instance to handlers [4]
- \li Access the \c MyStorage instance in handlers [5], [6]
- \li Insert the \l {Tasking::Storage} {Storage}<\c MyStorage> instance into a group [7]
- \endlist
-
- \section1 TaskTree class
-
- TaskTree executes the tree structure of asynchronous tasks according to the
- recipe described by the Group root element.
-
- As TaskTree is also an asynchronous task, it can be a part of another TaskTree.
- To place a nested TaskTree inside another TaskTree, insert the TaskTreeTask
- element into another Group element.
-
- TaskTree reports progress of completed tasks when running. The progress value
- is increased when a task finishes or is skipped or canceled.
- When TaskTree is finished and the TaskTree::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 TaskTree 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. \l {Tasking::Sync} {Sync} 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:
-
- \code
- Storage<CopyStorage> storage;
- const Group root = ...; // storage placed inside root's group and inside handlers
- TaskTree taskTree(root);
- auto initStorage = [](CopyStorage &storage) {
- storage.content = "initial content";
- };
- taskTree.onStorageSetup(storage, initStorage);
- taskTree.start();
- \endcode
-
- When the running task tree creates a \c 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:
-
- \code
- Storage<CopyStorage> storage;
- const Group root = ...; // storage placed inside root's group and inside handlers
- TaskTree taskTree(root);
- auto collectStorage = [](const CopyStorage &storage) {
- qDebug() << "final content" << storage.content;
- };
- taskTree.onStorageDone(storage, collectStorage);
- taskTree.start();
- \endcode
-
- When the running task tree is about to destroy a \c 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.
-
- \section1 Task Adapters
-
- To extend a TaskTree with a new task type, implement a simple adapter class
- derived from the TaskAdapter class template. The following class is an
- adapter for a single shot timer, which may be considered as a new asynchronous task:
-
- \code
- class TimerTaskAdapter : public TaskAdapter<QTimer>
- {
- public:
- TimerTaskAdapter() {
- task()->setSingleShot(true);
- task()->setInterval(1000);
- connect(task(), &QTimer::timeout, this, [this] { emit done(DoneResult::Success); });
- }
- private:
- void start() final { task()->start(); }
- };
-
- using TimerTask = CustomTask<TimerTaskAdapter>;
- \endcode
-
- You must derive the custom adapter from the TaskAdapter class template
- instantiated with a template parameter of the class implementing a running
- task. The code above uses QTimer to run the task. This class appears
- later as an argument to the task's handlers. The instance of this class
- parameter automatically becomes a member of the TaskAdapter template, and is
- accessible through the TaskAdapter::task() method. The constructor
- of \c TimerTaskAdapter initially configures the QTimer object and connects
- to the QTimer::timeout() signal. When the signal is triggered, \c TimerTaskAdapter
- emits the TaskInterface::done(DoneResult::Success) signal to inform the task tree that
- the task finished successfully. If it emits TaskInterface::done(DoneResult::Error),
- the task finished with an error.
- The TaskAdapter::start() method starts the timer.
-
- To make QTimer accessible inside TaskTree under the \c TimerTask name,
- define \c TimerTask to be an alias to the CustomTask<\c TimerTaskAdapter>.
- \c TimerTask becomes a new custom task type, using \c TimerTaskAdapter.
-
- The new task type is now registered, and you can use it in TaskTree:
-
- \code
- const auto onSetup = [](QTimer &task) { task.setInterval(2000); };
- const auto onDone = [] { qDebug() << "timer triggered"; };
- const Group root {
- TimerTask(onSetup, onDone)
- };
- \endcode
-
- When a task tree containing the root from the above example is started, it
- prints a debug message within two seconds and then finishes successfully.
-
- \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 object, 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 \c Deleter template parameter of the TaskAdapter.
-*/
-
-/*!
- Constructs an empty task tree. Use setRecipe() to pass a declarative description
- on how the task tree should execute the tasks and how it should handle the finished tasks.
-
- Starting an empty task tree is no-op and the relevant warning message is issued.
-
- \sa setRecipe(), start()
-*/
-TaskTree::TaskTree()
- : d(new TaskTreePrivate(this))
-{}
-
-/*!
- \overload
-
- Constructs a task tree with a given \a recipe. After the task tree is started,
- it executes the tasks contained inside the \a recipe and
- handles finished tasks according to the passed description.
-
- \sa setRecipe(), start()
-*/
-TaskTree::TaskTree(const Group &recipe) : TaskTree()
-{
- setRecipe(recipe);
-}
-
-/*!
- Destroys the task tree.
-
- When the task tree is running while being destructed, it cancels all the running tasks
- immediately. In this case, no handlers are called, not even the groups' and
- tasks' done handlers or onStorageDone() handlers. The task tree also doesn't emit any
- signals from the destructor, not even done() or progressValueChanged() signals.
- This behavior may always be relied on.
- It is completely safe to destruct the running task tree.
-
- It's a usual pattern to destruct the running task tree.
- It's guaranteed that the destruction will run quickly, without having to wait for
- the currently running tasks to finish, provided that the used tasks implement
- their destructors in a non-blocking way.
-
- \note Do not call the destructor directly from any of the running task's handlers
- or task tree's signals. In these cases, use \l deleteLater() instead.
-
- \sa cancel()
-*/
-TaskTree::~TaskTree()
-{
- QT_ASSERT(!d->m_guard.isLocked(), qWarning("Deleting TaskTree instance directly from "
- "one of its handlers will lead to a crash!"));
- // TODO: delete storages explicitly here?
- delete d;
-}
-
-/*!
- Sets a given \a recipe for the task tree. After the task tree is started,
- it executes the tasks contained inside the \a recipe and
- handles finished tasks according to the passed description.
-
- \note When called for a running task tree, the call is ignored.
-
- \sa TaskTree(const Tasking::Group &recipe), start()
-*/
-void TaskTree::setRecipe(const Group &recipe)
-{
- QT_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return);
- QT_ASSERT(!d->m_guard.isLocked(), qWarning("The setRecipe() is called from one of the"
- "TaskTree handlers, ignoring..."); return);
- // TODO: Should we clear the m_storageHandlers, too?
- d->m_storages.clear();
- d->m_root.emplace(d, recipe);
-}
-
-/*!
- Starts the task tree.
-
- Use setRecipe() or the constructor to set the declarative description according to which
- the task tree will execute the contained tasks and handle finished tasks.
-
- When the task tree is empty, that is, constructed with a default constructor,
- a call to \c start() is no-op and the relevant warning message is issued.
-
- Otherwise, when the task tree is already running, a call to \e start() is ignored and the
- relevant warning message is issued.
-
- Otherwise, the task tree is started.
-
- The started task tree may finish synchronously,
- for example when the main group's start handler returns SetupResult::StopWithError.
- For this reason, the connection to the done signal should be established before calling
- \c start(). Use isRunning() in order to detect whether the task tree is still running
- after a call to \c start().
-
- The task tree implementation relies on the running event loop.
- Make sure you have a QEventLoop or QCoreApplication or one of its
- subclasses running (or about to be run) when calling this method.
-
- \sa TaskTree(const Tasking::Group &), setRecipe(), isRunning(), cancel()
-*/
-void TaskTree::start()
-{
- QT_ASSERT(!isRunning(), qWarning("The TaskTree is already running, ignoring..."); return);
- QT_ASSERT(!d->m_guard.isLocked(), qWarning("The start() is called from one of the"
- "TaskTree handlers, ignoring..."); return);
- d->start();
-}
-
-/*!
- \fn void TaskTree::started()
-
- This signal is emitted when the task tree is started. The emission of this signal is
- followed synchronously by the progressValueChanged() signal with an initial \c 0 value.
-
- \sa start(), done()
-*/
-
-/*!
- \fn void TaskTree::done(DoneWith result)
-
- This signal is emitted when the task tree finished, passing the final \a result
- of the execution. The task tree neither calls any handler,
- nor emits any signal anymore after this signal was emitted.
-
- \note Do not delete the task tree directly from this signal's handler.
- Use deleteLater() instead.
-
- \sa started()
-*/
-
-/*!
- Cancels the execution of the running task tree.
-
- Cancels all the running tasks immediately.
- All running tasks finish with an error, invoking their error handlers.
- All running groups dispatch their handlers according to their workflow policies,
- invoking their done handlers. The storages' onStorageDone() handlers are invoked, too.
- The progressValueChanged() signals are also being sent.
- This behavior may always be relied on.
-
- The \c cancel() function is executed synchronously, so that after a call to \c cancel()
- all running tasks are finished and the tree is already canceled.
- It's guaranteed that \c cancel() will run quickly, without any blocking wait for
- the currently running tasks to finish, provided the used tasks implement their destructors
- in a non-blocking way.
-
- When the task tree is empty, that is, constructed with a default constructor,
- a call to \c cancel() is no-op and the relevant warning message is issued.
-
- Otherwise, when the task tree wasn't started, a call to \c cancel() is ignored.
-
- \note Do not call this function directly from any of the running task's handlers
- or task tree's signals.
-
- \sa ~TaskTree()
-*/
-void TaskTree::cancel()
-{
- QT_ASSERT(!d->m_guard.isLocked(), qWarning("The cancel() is called from one of the"
- "TaskTree handlers, ignoring..."); return);
- d->stop();
-}
-
-/*!
- Returns \c true if the task tree is currently running; otherwise returns \c false.
-
- \sa start(), cancel()
-*/
-bool TaskTree::isRunning() const
-{
- return bool(d->m_runtimeRoot);
-}
-
-/*!
- Executes a local event loop with QEventLoop::ExcludeUserInputEvents and starts the task tree.
-
- Returns DoneWith::Success if the task tree finished successfully;
- otherwise returns DoneWith::Error.
-
- \note Avoid using this method from the main thread. Use asynchronous start() instead.
- This method is to be used in non-main threads or in auto tests.
-
- \sa start()
-*/
-DoneWith TaskTree::runBlocking()
-{
- QPromise<void> dummy;
- dummy.start();
- return runBlocking(dummy.future());
-}
-
-/*!
- \overload runBlocking()
-
- The passed \a future is used for listening to the cancel event.
- When the task tree is canceled, this method cancels the passed \a future.
-*/
-DoneWith TaskTree::runBlocking(const QFuture<void> &future)
-{
- if (future.isCanceled())
- return DoneWith::Cancel;
-
- DoneWith doneWith = DoneWith::Cancel;
- QEventLoop loop;
- connect(this, &TaskTree::done, &loop, [&loop, &doneWith](DoneWith result) {
- doneWith = result;
- // Otherwise, the tasks from inside the running tree that were deleteLater()
- // will be leaked. Refer to the QObject::deleteLater() docs.
- QMetaObject::invokeMethod(&loop, [&loop] { loop.quit(); }, Qt::QueuedConnection);
- });
- QFutureWatcher<void> watcher;
- connect(&watcher, &QFutureWatcherBase::canceled, this, &TaskTree::cancel);
- watcher.setFuture(future);
-
- QTimer::singleShot(0, this, &TaskTree::start);
-
- loop.exec(QEventLoop::ExcludeUserInputEvents);
- if (doneWith == DoneWith::Cancel) {
- auto nonConstFuture = future;
- nonConstFuture.cancel();
- }
- return doneWith;
-}
-
-/*!
- Constructs a temporary task tree using the passed \a recipe and runs it blocking.
-
- Returns DoneWith::Success if the task tree finished successfully;
- otherwise returns DoneWith::Error.
-
- \note Avoid using this method from the main thread. Use asynchronous start() instead.
- This method is to be used in non-main threads or in auto tests.
-
- \sa start()
-*/
-DoneWith TaskTree::runBlocking(const Group &recipe)
-{
- QPromise<void> dummy;
- dummy.start();
- return TaskTree::runBlocking(recipe, dummy.future());
-}
-
-/*!
- \overload runBlocking(const Group &recipe)
-
- The passed \a future is used for listening to the cancel event.
- When the task tree is canceled, this method cancels the passed \a future.
-*/
-DoneWith TaskTree::runBlocking(const Group &recipe, const QFuture<void> &future)
-{
- TaskTree taskTree(recipe);
- return taskTree.runBlocking(future);
-}
-
-/*!
- Returns the current real count of asynchronous chains of invocations.
-
- The returned value indicates how many times the control returns to the caller's
- event loop while the task tree is running. Initially, this value is 0.
- If the execution of the task tree finishes fully synchronously, this value remains 0.
- If the task tree contains any asynchronous tasks that are successfully started during
- a call to start(), this value is bumped to 1 just before the call to start() finishes.
- Later, when any asynchronous task finishes and any possible continuations are started,
- this value is bumped again. The bumping continues until the task tree finishes.
- When the task tree emits the done() signal, the bumping stops.
- The asyncCountChanged() signal is emitted on every bump of this value.
-
- \sa asyncCountChanged()
-*/
-int TaskTree::asyncCount() const
-{
- return d->m_asyncCount;
-}
-
-/*!
- \fn void TaskTree::asyncCountChanged(int count)
-
- This signal is emitted when the running task tree is about to return control to the caller's
- event loop. When the task tree is started, this signal is emitted with \a count value of 0,
- and emitted later on every asyncCount() value bump with an updated \a count value.
- Every signal sent (except the initial one with the value of 0) guarantees that the task tree
- is still running asynchronously after the emission.
-
- \sa asyncCount()
-*/
-
-/*!
- Returns the number of asynchronous tasks contained in the stored recipe.
-
- \note The returned number doesn't include \l {Tasking::Sync} {Sync} tasks.
- \note Any task or group that was set up using withTimeout() increases the total number of
- tasks by \c 1.
-
- \sa setRecipe(), progressMaximum()
-*/
-int TaskTree::taskCount() const
-{
- return d->m_root ? d->m_root->taskCount() : 0;
-}
-
-/*!
- \fn void TaskTree::progressValueChanged(int value)
-
- This signal is emitted when the running task tree finished, canceled, or skipped some tasks.
- The \a value gives the current total number of finished, canceled or skipped tasks.
- When the task tree is started, and after the started() signal was emitted,
- this signal is emitted with an initial \a value of \c 0.
- When the task tree is about to finish, and before the done() signal is emitted,
- this signal is emitted with the final \a value of progressMaximum().
-
- \sa progressValue(), progressMaximum()
-*/
-
-/*!
- \fn int TaskTree::progressMaximum() const
-
- Returns the maximum progressValue().
-
- \note Currently, it's the same as taskCount(). This might change in the future.
-
- \sa progressValue()
-*/
-
-/*!
- Returns the current progress value, which is between the \c 0 and progressMaximum().
-
- The returned number indicates how many tasks have been already finished, canceled, or skipped
- while the task tree is running.
- When the task tree is started, this number is set to \c 0.
- When the task tree is finished, this number always equals progressMaximum().
-
- \sa progressMaximum(), progressValueChanged()
-*/
-int TaskTree::progressValue() const
-{
- return d->m_progressValue;
-}
-
-/*!
- \fn template <typename StorageStruct, typename Handler> void TaskTree::onStorageSetup(const Storage<StorageStruct> &storage, Handler &&handler)
-
- Installs a storage setup \a handler for the \a storage to pass the initial data
- dynamically to the running task tree.
-
- The \c StorageHandler takes a \e reference to the \c StorageStruct instance:
-
- \code
- static void save(const QString &fileName, const QByteArray &array) { ... }
-
- Storage<QByteArray> storage;
-
- const auto onSaverSetup = [storage](ConcurrentCall<QByteArray> &concurrent) {
- concurrent.setConcurrentCallData(&save, "foo.txt", *storage);
- };
-
- const Group root {
- storage,
- ConcurrentCallTask(onSaverSetup)
- };
-
- TaskTree taskTree(root);
- auto initStorage = [](QByteArray &storage){
- storage = "initial content";
- };
- taskTree.onStorageSetup(storage, initStorage);
- taskTree.start();
- \endcode
-
- When the running task tree enters a Group where the \a storage is placed in,
- it creates a \c StorageStruct instance, ready to be used inside this group.
- Just after the \c StorageStruct instance is created, and before any handler of this group
- is called, the task tree invokes the passed \a handler. This enables setting up
- initial content for the given storage dynamically. Later, when any group's handler is invoked,
- the task tree activates the created and initialized storage, so that it's available inside
- any group's handler.
-
- \sa onStorageDone()
-*/
-
-/*!
- \fn template <typename StorageStruct, typename Handler> void TaskTree::onStorageDone(const Storage<StorageStruct> &storage, Handler &&handler)
-
- Installs a storage done \a handler for the \a storage to retrieve the final data
- dynamically from the running task tree.
-
- The \c StorageHandler takes a \c const \e reference to the \c StorageStruct instance:
-
- \code
- static QByteArray load(const QString &fileName) { ... }
-
- Storage<QByteArray> storage;
-
- const auto onLoaderSetup = [](ConcurrentCall<QByteArray> &concurrent) {
- concurrent.setConcurrentCallData(&load, "foo.txt");
- };
- const auto onLoaderDone = [storage](const ConcurrentCall<QByteArray> &concurrent) {
- *storage = concurrent.result();
- };
-
- const Group root {
- storage,
- ConcurrentCallTask(onLoaderSetup, onLoaderDone, CallDoneIf::Success)
- };
-
- TaskTree taskTree(root);
- auto collectStorage = [](const QByteArray &storage){
- qDebug() << "final content" << storage;
- };
- taskTree.onStorageDone(storage, collectStorage);
- taskTree.start();
- \endcode
-
- When the running task tree is about to leave a Group where the \a storage is placed in,
- it destructs a \c StorageStruct instance.
- Just before the \c StorageStruct instance is destructed, and after all possible handlers from
- this group were called, the task tree invokes the passed \a handler. This enables reading
- the final content of the given storage dynamically and processing it further outside of
- the task tree.
-
- This handler is called also when the running tree is canceled. However, it's not called
- when the running tree is destructed.
-
- \sa onStorageSetup()
-*/
-
-void TaskTree::setupStorageHandler(const StorageBase &storage,
- const StorageBase::StorageHandler &setupHandler,
- const StorageBase::StorageHandler &doneHandler)
-{
- auto it = d->m_storageHandlers.find(storage);
- if (it == d->m_storageHandlers.end()) {
- d->m_storageHandlers.insert(storage, {setupHandler, doneHandler});
- return;
- }
- if (setupHandler) {
- QT_ASSERT(!it->m_setupHandler,
- qWarning("The storage has its setup handler defined, overriding..."));
- it->m_setupHandler = setupHandler;
- }
- if (doneHandler) {
- QT_ASSERT(!it->m_doneHandler,
- qWarning("The storage has its done handler defined, overriding..."));
- it->m_doneHandler = doneHandler;
- }
-}
-
-TaskTreeTaskAdapter::TaskTreeTaskAdapter()
-{
- connect(task(), &TaskTree::done, this,
- [this](DoneWith result) { emit done(toDoneResult(result)); });
-}
-
-void TaskTreeTaskAdapter::start()
-{
- task()->start();
-}
-
-using TimeoutCallback = std::function<void()>;
-
-struct TimerData
-{
- system_clock::time_point m_deadline;
- QPointer<QObject> m_context;
- TimeoutCallback m_callback;
-};
-
-struct TimerThreadData
-{
- Q_DISABLE_COPY_MOVE(TimerThreadData)
-
- TimerThreadData() = default; // defult constructor is required for initializing with {} since C++20 by Mingw 11.20
- QHash<int, TimerData> m_timerIdToTimerData = {};
- QMap<system_clock::time_point, QList<int>> m_deadlineToTimerId = {};
- int m_timerIdCounter = 0;
-};
-
-// Please note the thread_local keyword below guarantees a separate instance per thread.
-static thread_local TimerThreadData s_threadTimerData = {};
-
-static void removeTimerId(int timerId)
-{
- const auto it = s_threadTimerData.m_timerIdToTimerData.constFind(timerId);
- QT_ASSERT(it != s_threadTimerData.m_timerIdToTimerData.cend(),
- qWarning("Removing active timerId failed."); return);
-
- const system_clock::time_point deadline = it->m_deadline;
- s_threadTimerData.m_timerIdToTimerData.erase(it);
-
- QList<int> &ids = s_threadTimerData.m_deadlineToTimerId[deadline];
- const int removedCount = ids.removeAll(timerId);
- QT_ASSERT(removedCount == 1, qWarning("Removing active timerId failed."); return);
- if (ids.isEmpty())
- s_threadTimerData.m_deadlineToTimerId.remove(deadline);
-}
-
-static void handleTimeout(int timerId)
-{
- const auto itData = s_threadTimerData.m_timerIdToTimerData.constFind(timerId);
- if (itData == s_threadTimerData.m_timerIdToTimerData.cend())
- return; // The timer was already activated.
-
- const auto deadline = itData->m_deadline;
- while (true) {
- auto itMap = s_threadTimerData.m_deadlineToTimerId.begin();
- if (itMap == s_threadTimerData.m_deadlineToTimerId.end())
- return;
-
- if (itMap.key() > deadline)
- return;
-
- std::optional<TimerData> timerData;
- QList<int> &idList = *itMap;
- if (!idList.isEmpty()) {
- const int first = idList.first();
- idList.removeFirst();
-
- const auto it = s_threadTimerData.m_timerIdToTimerData.constFind(first);
- if (it != s_threadTimerData.m_timerIdToTimerData.cend()) {
- timerData = it.value();
- s_threadTimerData.m_timerIdToTimerData.erase(it);
- } else {
- QT_CHECK(false);
- }
- } else {
- QT_CHECK(false);
- }
-
- if (idList.isEmpty())
- s_threadTimerData.m_deadlineToTimerId.erase(itMap);
- if (timerData && timerData->m_context)
- timerData->m_callback();
- }
-}
-
-static int scheduleTimeout(milliseconds timeout, QObject *context, const TimeoutCallback &callback)
-{
- const int timerId = ++s_threadTimerData.m_timerIdCounter;
- const system_clock::time_point deadline = system_clock::now() + timeout;
- QTimer::singleShot(timeout, context, [timerId] { handleTimeout(timerId); });
- s_threadTimerData.m_timerIdToTimerData.emplace(timerId, TimerData{deadline, context, callback});
- s_threadTimerData.m_deadlineToTimerId[deadline].append(timerId);
- return timerId;
-}
-
-TimeoutTaskAdapter::TimeoutTaskAdapter()
-{
- *task() = milliseconds::zero();
-}
-
-TimeoutTaskAdapter::~TimeoutTaskAdapter()
-{
- if (m_timerId)
- removeTimerId(*m_timerId);
-}
-
-void TimeoutTaskAdapter::start()
-{
- m_timerId = scheduleTimeout(*task(), this, [this] {
- m_timerId.reset();
- emit done(DoneResult::Success);
- });
-}
-
-ExecutableItem timeoutTask(const std::chrono::milliseconds &timeout, DoneResult result)
-{
- return TimeoutTask([timeout](std::chrono::milliseconds &t) { t = timeout; }, result);
-}
-
-/*!
- \typealias Tasking::TaskTreeTask
-
- Type alias for the CustomTask, to be used inside recipes, associated with the TaskTree task.
-*/
-
-/*!
- \typealias Tasking::TimeoutTask
-
- Type alias for the CustomTask, to be used inside recipes, associated with the
- \c std::chrono::milliseconds type. \c std::chrono::milliseconds is used to set up the
- timeout duration. The default timeout is \c std::chrono::milliseconds::zero(), that is,
- the TimeoutTask finishes as soon as the control returns to the running event loop.
-
- Example usage:
-
- \code
- using namespace std::chrono;
- using namespace std::chrono_literals;
-
- const auto onSetup = [](milliseconds &timeout) { timeout = 1000ms; }
- const auto onDone = [] { qDebug() << "Timed out."; }
-
- const Group root {
- Timeout(onSetup, onDone)
- };
- \endcode
-*/
-
-} // namespace Tasking
-
-QT_END_NAMESPACE