forked from qt-creator/qt-creator
TaskTree: Add more docs
Change-Id: Ie87ff78220d35266cc6e23ade7a1b4df4f483218 Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io> Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
@@ -589,46 +589,715 @@ void TaskNode::invokeEndHandler(bool success)
|
|||||||
|
|
||||||
/*!
|
/*!
|
||||||
\class Utils::TaskTree
|
\class Utils::TaskTree
|
||||||
|
\inheaderfile utils/tasktree.h
|
||||||
\brief The TaskTree class is responsible for running async task tree structure defined in a
|
\inmodule QtCreator
|
||||||
|
\ingroup mainclasses
|
||||||
|
\brief The TaskTree class runs an async task tree structure defined in a
|
||||||
declarative way.
|
declarative way.
|
||||||
|
|
||||||
The Tasking namespace (similar to Layouting) is designer for building declarative task
|
Use the Tasking namespace to build extensible, declarative task tree
|
||||||
tree structure. The examples of tasks that can be used inside TaskTree are e.g. QtcProcess,
|
structures that contain possibly asynchronous tasks, such as QtcProcess,
|
||||||
FileTransfer, AsyncTask<>. It's extensible, so any possible asynchronous task may be
|
FileTransfer, or AsyncTask<ReturnType>. TaskTree structures enable you
|
||||||
integrated and used inside TaskTree. TaskTree enables to form sophisticated mixtures of
|
to create a sophisticated mixture of a parallel or sequential flow of tasks
|
||||||
parallel or sequential flow of tasks in tree form.
|
in the form of a tree and to run it any time later.
|
||||||
|
|
||||||
The TaskTree consist of Group root element. The Group can have nested Group elements.
|
\section1 Root Element and Tasks
|
||||||
The Group may also contain any number of tasks, e.g. Process, FileTransfer,
|
|
||||||
AsyncTask<ReturnType>.
|
|
||||||
|
|
||||||
Each Group can contain various other elements describing the processing flow.
|
The TaskTree has a mandatory Group root element, which may contain
|
||||||
|
any number of tasks of various types, such as Process, FileTransfer,
|
||||||
|
or AsyncTask<ReturnType>:
|
||||||
|
|
||||||
The execute mode elements of a Group specify how direct children of a Group will be executed.
|
\code
|
||||||
The "sequential" element of a Group means all tasks in a group will be executed in chain,
|
using namespace Utils;
|
||||||
so after the previous task finished, the next will be started. This is the default Group
|
using namespace Tasking;
|
||||||
behavior. The "parallel" element of a Group means that all tasks in a Group will be started
|
|
||||||
simultaneously. When having nested Groups hierarchy, we may mix execute modes freely
|
|
||||||
and each Group will be executed according to its own execute mode.
|
|
||||||
The "sequential" mode may be very useful in cases when result data from one task is need to
|
|
||||||
be passed as an input data to the other task - sequential mode guarantees that the next
|
|
||||||
task will be started only after the previous task has already finished.
|
|
||||||
|
|
||||||
There are many possible "workflow" behaviors for the Group. E.g. "stopOnError",
|
const Group root {
|
||||||
the default Group workflow behavior, means that whenever any direct child of a Group
|
Process(...),
|
||||||
finished with error, we immediately stop processing other tasks in this group
|
Async<int>(...),
|
||||||
(in parallel case) by canceling them and immediately finish the Group with error.
|
Transfer(...)
|
||||||
|
};
|
||||||
|
|
||||||
The user of TaskTree specifies how to setup his tasks (by providing TaskSetupHandlers)
|
TaskTree *taskTree = new TaskTree(root);
|
||||||
and how to collect output data from the finished tasks (by providing TaskEndHandlers).
|
connect(taskTree, &TaskTree::done, ...); // a successfully finished handler
|
||||||
The user don't need to create tasks manually - TaskTree will create them when it's needed
|
connect(taskTree, &TaskTree::errorOccurred, ...); // an erroneously finished handler
|
||||||
and destroy when they are not used anymore.
|
taskTree->start();
|
||||||
|
\endcode
|
||||||
|
|
||||||
Whenever a Group elemenent is being started, the Group's OnGroupSetup handler is being called.
|
The task tree above has a top level element of the Group type that contains
|
||||||
Just after the handler finishes, all Group's children are executed (either in parallel or
|
tasks of the type QtcProcess, FileTransfer, and AsyncTask<int>.
|
||||||
in sequence). When all Group's children finished, one of Group's OnGroupDone or OnGroupError
|
After taskTree->start() is called, the tasks are run in a chain, starting
|
||||||
is being executed, depending on results of children execution and Group's workflow policy.
|
with Process. When Process finishes successfully, the Async<int> task is
|
||||||
|
started. Finally, when the asynchronous task finishes successfully, the
|
||||||
|
FileTransfer task is started.
|
||||||
|
|
||||||
|
When the last running task finishes with success, the task tree is considered
|
||||||
|
to have run successfully and the TaskTree::done() signal is emitted.
|
||||||
|
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::errorOccurred() signal.
|
||||||
|
|
||||||
|
\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,
|
||||||
|
Process(...),
|
||||||
|
Async<int>(...)
|
||||||
|
},
|
||||||
|
Transfer(...)
|
||||||
|
};
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
The example above differs from the first example in that the root element has
|
||||||
|
a subgroup that contains the Process and Async<int> tasks. The subgroup is a
|
||||||
|
sibling element of the Transfer task 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 Process and Async<int> tasks 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
|
||||||
|
Transfer task 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 (Process or Async<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 Process starts
|
||||||
|
\li Process starts
|
||||||
|
\row
|
||||||
|
\li Async<int> starts
|
||||||
|
\li Async<int> starts
|
||||||
|
\row
|
||||||
|
\li ...
|
||||||
|
\li ...
|
||||||
|
\row
|
||||||
|
\li \b {Process finishes}
|
||||||
|
\li \b {Async<int> finishes}
|
||||||
|
\row
|
||||||
|
\li ...
|
||||||
|
\li ...
|
||||||
|
\row
|
||||||
|
\li \b {Async<int> finishes}
|
||||||
|
\li \b {Process finishes}
|
||||||
|
\row
|
||||||
|
\li Sub Group finishes
|
||||||
|
\li Sub Group finishes
|
||||||
|
\row
|
||||||
|
\li Transfer starts
|
||||||
|
\li Transfer starts
|
||||||
|
\row
|
||||||
|
\li ...
|
||||||
|
\li ...
|
||||||
|
\row
|
||||||
|
\li Transfer finishes
|
||||||
|
\li Transfer 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 Process finishes with an error while Async<int> is still being executed,
|
||||||
|
Async<int> is automatically stopped, the subgroup finishes with an error,
|
||||||
|
Transfer 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 Process task inside a task tree is associated with
|
||||||
|
the QtcProcess 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 Process tasks, and the task tree
|
||||||
|
executes the group, it creates an instance of QtcProcess for the first
|
||||||
|
Process task and starts it. If the QtcProcess instance finishes successfully,
|
||||||
|
the task tree destructs it and creates a new QtcProcess instance for the
|
||||||
|
second Process, and so on. If the first task finishes with an error, the task
|
||||||
|
tree stops creating QtcProcess 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 Process
|
||||||
|
\li Utils::QtcProcess
|
||||||
|
\li Starts processes.
|
||||||
|
\row
|
||||||
|
\li Async<ReturnType>
|
||||||
|
\li Utils::AsyncTask<ReturnType>
|
||||||
|
\li Starts asynchronous tasks; run in separate thread.
|
||||||
|
\row
|
||||||
|
\li Tree
|
||||||
|
\li Utils::TaskTree
|
||||||
|
\li Starts a nested task tree.
|
||||||
|
\row
|
||||||
|
\li Transfer
|
||||||
|
\li ProjectExplorer::FileTransfer
|
||||||
|
\li Starts file transfer between different devices.
|
||||||
|
\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 Start Handler
|
||||||
|
|
||||||
|
When a corresponding task class object is created and before it's started,
|
||||||
|
the task tree invokes a mandatory user-provided setup handler. The setup
|
||||||
|
handler should always take a \e reference to the associated task class object:
|
||||||
|
|
||||||
|
\code
|
||||||
|
const auto onSetup = [](QtcProcess &process) {
|
||||||
|
process.setCommand({"sleep", {"3"}});
|
||||||
|
};
|
||||||
|
const Group root {
|
||||||
|
Process(onSetup)
|
||||||
|
};
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
You can modify the passed QtcProcess in the setup handler, so that the task
|
||||||
|
tree can start the process according to your configuration.
|
||||||
|
You do not need to call \e {process.start();} in the setup handler,
|
||||||
|
as the task tree calls it when needed. The setup handler is mandatory
|
||||||
|
and must be the first argument of the task's constructor.
|
||||||
|
|
||||||
|
Optionally, the setup handler may return a TaskAction. The returned
|
||||||
|
TaskAction influences the further start behavior of a given task. The
|
||||||
|
possible values are:
|
||||||
|
|
||||||
|
\table
|
||||||
|
\header
|
||||||
|
\li TaskAction Value
|
||||||
|
\li Brief Description
|
||||||
|
\row
|
||||||
|
\li Continue
|
||||||
|
\li The task is started normally. This is the default behavior when the
|
||||||
|
setup handler doesn't return TaskAction (that is, its return type is
|
||||||
|
void).
|
||||||
|
\row
|
||||||
|
\li StopWithDone
|
||||||
|
\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. 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 and Error Handlers
|
||||||
|
|
||||||
|
When a running task finishes, the task tree invokes an optionally provided
|
||||||
|
done or error handler. Both handlers should always take a \e {const reference}
|
||||||
|
to the associated task class object:
|
||||||
|
|
||||||
|
\code
|
||||||
|
const auto onSetup = [](QtcProcess &process) {
|
||||||
|
process.setCommand({"sleep", {"3"}});
|
||||||
|
};
|
||||||
|
const auto onDone = [](const QtcProcess &process) {
|
||||||
|
qDebug() << "Success" << process.cleanedStdOut();
|
||||||
|
};
|
||||||
|
const auto onError = [](const QtcProcess &process) {
|
||||||
|
qDebug() << "Failure" << process.cleanedStdErr();
|
||||||
|
};
|
||||||
|
const Group root {
|
||||||
|
Process(onSetup, onDone, onError)
|
||||||
|
};
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
The done and error handlers may collect output data from QtcProcess, and store it
|
||||||
|
for further processing or perform additional actions. The done handler is optional.
|
||||||
|
When used, it must be the second argument of the task constructor.
|
||||||
|
The error handler must always be the third argument.
|
||||||
|
You can omit the handlers or substitute the ones that you do not need with curly braces ({}).
|
||||||
|
|
||||||
|
\note If the task setup handler returns StopWithDone or StopWithError,
|
||||||
|
neither the done nor error handler is 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 onGroupSetup = [] {
|
||||||
|
qDebug() << "Entering the group";
|
||||||
|
};
|
||||||
|
const Group root {
|
||||||
|
OnGroupSetup(onGroupSetup),
|
||||||
|
Process(...)
|
||||||
|
};
|
||||||
|
\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 start handler, the group start handler may return TaskAction.
|
||||||
|
The returned TaskAction 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 TaskAction::Continue, so that all
|
||||||
|
tasks are started normally. Otherwise, when the start handler returns
|
||||||
|
TaskAction::StopWithDone or TaskAction::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 TaskAction::Continue; }),
|
||||||
|
Process(...) // Process 1
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
OnGroupSetup([] { qDebug() << "Group 2 setup"; return TaskAction::StopWithDone; }),
|
||||||
|
Process(...) // Process 2
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
OnGroupSetup([] { qDebug() << "Group 3 setup"; return TaskAction::StopWithError; }),
|
||||||
|
Process(...) // Process 3
|
||||||
|
},
|
||||||
|
Process(...) // 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 TaskAction, 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 StopWithDone, 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 and Error Handlers
|
||||||
|
|
||||||
|
A Group's done or error handler is executed after the successful or failed
|
||||||
|
execution of its tasks, respectively. The final value reported by the
|
||||||
|
group depends on its \l {Workflow Policy}. The handlers can apply other
|
||||||
|
necessary actions. The done and error handlers are defined inside the
|
||||||
|
OnGroupDone and OnGroupError elements of a group, respectively. They do not
|
||||||
|
take arguments:
|
||||||
|
|
||||||
|
\code
|
||||||
|
const Group root {
|
||||||
|
OnGroupSetup([] { qDebug() << "Root setup"; }),
|
||||||
|
Process(...),
|
||||||
|
OnGroupDone([] { qDebug() << "Root finished with success"; }),
|
||||||
|
OnGroupError([] { qDebug() << "Root finished with error"; })
|
||||||
|
};
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
The group done and error handlers are optional. If you add more than one
|
||||||
|
OnGroupDone or OnGroupError each to a group, an assert is triggered at
|
||||||
|
runtime that includes an error message.
|
||||||
|
|
||||||
|
\note Even if the group setup handler returns StopWithDone or StopWithError,
|
||||||
|
one of the task's done or error handlers is invoked. This behavior differs
|
||||||
|
from that of task handlers 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.
|
||||||
|
|
||||||
|
\table
|
||||||
|
\header
|
||||||
|
\li Execution Mode
|
||||||
|
\li Description
|
||||||
|
\row
|
||||||
|
\li sequential
|
||||||
|
\li Default. 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.
|
||||||
|
\row
|
||||||
|
\li parallel
|
||||||
|
\li All the direct child tasks of a group are started after the group is
|
||||||
|
started, without waiting for the previous tasks to finish. In this
|
||||||
|
mode, all tasks run simultaneously.
|
||||||
|
\row
|
||||||
|
\li ParallelLimit(int limit)
|
||||||
|
\li In this mode, a limited number of direct child tasks run simultaneously.
|
||||||
|
The \e limit defines the maximum number of tasks running in parallel
|
||||||
|
in a group. When the group is started, the first batch tasks is
|
||||||
|
started (the number of tasks in batch equals to passed limit, at most),
|
||||||
|
while the others are kept waiting. When a 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. The value 1 means \e sequential
|
||||||
|
execution. The value 0 means unlimited and equals \e parallel.
|
||||||
|
\endtable
|
||||||
|
|
||||||
|
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 (in a nested tree), 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 its direct child tasks finish:
|
||||||
|
|
||||||
|
\table
|
||||||
|
\header
|
||||||
|
\li Workflow Policy
|
||||||
|
\li Description
|
||||||
|
\row
|
||||||
|
\li stopOnError
|
||||||
|
\li Default. If a task finishes with an error, the group:
|
||||||
|
\list 1
|
||||||
|
\li Stops the running tasks (if any - for example, in parallel
|
||||||
|
mode).
|
||||||
|
\li Skips executing tasks it has not started (for example, in the
|
||||||
|
sequential mode).
|
||||||
|
\li Immediately finishes with an error.
|
||||||
|
\endlist
|
||||||
|
If all child tasks finish successfully or the group is empty, the group
|
||||||
|
finishes with success.
|
||||||
|
\row
|
||||||
|
\li continueOnError
|
||||||
|
\li 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 group finished with success.
|
||||||
|
If a task finishes with an error, the group:
|
||||||
|
\list 1
|
||||||
|
\li Continues executing the tasks that are running or have not
|
||||||
|
started yet.
|
||||||
|
\li Finishes with an error when all tasks finish.
|
||||||
|
\endlist
|
||||||
|
If all tasks finish successfully or the group is empty, the group
|
||||||
|
finishes with success.
|
||||||
|
\row
|
||||||
|
\li stopOnDone
|
||||||
|
\li If a task finishes with success, the group:
|
||||||
|
\list 1
|
||||||
|
\li Stops running tasks and skips those that it has not started.
|
||||||
|
\li Immediately finishes with success.
|
||||||
|
\endlist
|
||||||
|
If all tasks finish with an error or the group is empty, the group
|
||||||
|
finishes with an error.
|
||||||
|
\row
|
||||||
|
\li continueOnDone
|
||||||
|
\li Similar to stopOnDone, 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 group finished with an error.
|
||||||
|
If a task finishes with success, the group:
|
||||||
|
\list 1
|
||||||
|
\li Continues executing the tasks that are running or have not
|
||||||
|
started yet.
|
||||||
|
\li Finishes with success when all tasks finish.
|
||||||
|
\endlist
|
||||||
|
If all tasks finish with an error or the group is empty, the group
|
||||||
|
finishes with an error.
|
||||||
|
\row
|
||||||
|
\li optional
|
||||||
|
\li The group executes all tasks and ignores their return state. If all
|
||||||
|
tasks finish or the group is empty, the group finishes with success.
|
||||||
|
\endtable
|
||||||
|
|
||||||
|
If a child of a group is also a group (in a nested tree), the child group
|
||||||
|
runs its tasks according to its own workflow policy.
|
||||||
|
|
||||||
|
\section2 Storage
|
||||||
|
|
||||||
|
Use the Storage element to exchange information between tasks. Especially,
|
||||||
|
in the sequential execution mode, when a task needs data from another 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 FilePath &fileName) { ... }
|
||||||
|
static void save(const FilePath &fileName, const QByteArray &array) { ... }
|
||||||
|
|
||||||
|
static TaskItem diffRecipe(const FilePath &source, const FilePath &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 TreeStorage<CopyStorage> storage;
|
||||||
|
|
||||||
|
const auto onLoaderSetup = [source](Async<QByteArray> &async) {
|
||||||
|
async.setAsyncCallData(&load, source);
|
||||||
|
};
|
||||||
|
// [4] runtime: task tree activates the instance from [5] before invoking handler
|
||||||
|
const auto onLoaderDone = [storage](const Async<QByteArray> &async) {
|
||||||
|
storage->content = async.result();
|
||||||
|
};
|
||||||
|
|
||||||
|
// [4] runtime: task tree activates the instance from [5] before invoking handler
|
||||||
|
const auto onSaverSetup = [storage, destination](Async<void> &async) {
|
||||||
|
async.setAsyncCallData(&save, destination, storage->content);
|
||||||
|
};
|
||||||
|
const auto onSaverDone = [](const Async<void> &async) {
|
||||||
|
qDebug() << "Save done successfully";
|
||||||
|
};
|
||||||
|
|
||||||
|
const Group root {
|
||||||
|
// [5] runtime: task tree creates an instance of CopyStorage when root is entered
|
||||||
|
Storage(storage),
|
||||||
|
Async<QByteArray>(onLoaderSetup, onLoaderDone),
|
||||||
|
Async<void>(onSaverSetup, onSaverDone)
|
||||||
|
};
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
In the example above, the inter-task data consists of a QByteArray content
|
||||||
|
variable [2] enclosed in a CopyStorage custom struct [1]. If the loader
|
||||||
|
finishes successfully, it stores the data in a CopyStorage::content
|
||||||
|
variable. The saver then uses the variable to configure the saving task.
|
||||||
|
|
||||||
|
To enable a task tree to manage the CopyStorage struct, an instance of
|
||||||
|
TreeStorage<CopyStorage> is created [3]. If a copy of this object is
|
||||||
|
inserted as group's child task [5], an instance of CopyStorage struct is
|
||||||
|
created dynamically when the task tree enters this group. When the task
|
||||||
|
tree leaves this group, the existing instance of CopyStorage struct is
|
||||||
|
destructed as it's no longer needed.
|
||||||
|
|
||||||
|
If several task trees that hold a copy of the common TreeStorage<CopyStorage>
|
||||||
|
instance run simultaneously, each task tree contains its own copy of the
|
||||||
|
CopyStorage struct.
|
||||||
|
|
||||||
|
You can access CopyStorage from any handler in the group with a storage object.
|
||||||
|
This includes all handlers of all descendant tasks of the group with
|
||||||
|
a storage object. To access the custom struct in a handler, pass the
|
||||||
|
copy of the TreeStorage<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 [5],
|
||||||
|
the task tree activates its own CopyStorage instance inside the
|
||||||
|
TreeStorage<CopyStorage> object. Therefore, the CopyStorage struct may be
|
||||||
|
accessed only from within the handler body. To access the currently active
|
||||||
|
CopyStorage from within TreeStorage<CopyStorage>, use the TreeStorage::operator->()
|
||||||
|
or TreeStorage::activeStorage() method.
|
||||||
|
|
||||||
|
The following list summarizes how to employ a Storage object into the task
|
||||||
|
tree:
|
||||||
|
\list 1
|
||||||
|
\li Define the custom structure MyStorage with custom data [1], [2]
|
||||||
|
\li Create an instance of TreeStorage<MyStorage> storage [3]
|
||||||
|
\li Pass the TreeStorage<MyStorage> instance to handlers [4]
|
||||||
|
\li Insert the TreeStorage<MyStorage> instance into a group [5]
|
||||||
|
\endlist
|
||||||
|
|
||||||
|
\note The current implementation assumes that all running task trees
|
||||||
|
containing copies of the same TreeStorage run in the same thread. Otherwise,
|
||||||
|
the behavior is undefined.
|
||||||
|
|
||||||
|
\section1 TaskTree
|
||||||
|
|
||||||
|
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 Tasking::Tree
|
||||||
|
element into other tree's Group element.
|
||||||
|
|
||||||
|
TaskTree reports progress of completed tasks when running. The progress value
|
||||||
|
is increased when a task finishes or is skipped or stopped.
|
||||||
|
When TaskTree is finished and the TaskTree::done() or TaskTree::errorOccurred()
|
||||||
|
signal is emitted, the current value of the progress equals the maximum
|
||||||
|
progress value. Maximum progress equals the total number of 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
TreeStorage<CopyStorage> storage;
|
||||||
|
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 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
|
||||||
|
TreeStorage<CopyStorage> storage;
|
||||||
|
Group root = ...; // storage placed inside root's group and inside handlers
|
||||||
|
TaskTree taskTree(root);
|
||||||
|
auto collectStorage = [](CopyStorage *storage){
|
||||||
|
qDebug() << "final content" << storage->content;
|
||||||
|
};
|
||||||
|
taskTree.onStorageDone(storage, collectStorage);
|
||||||
|
taskTree.start();
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
When the running task tree is about to destroy a CopyStorage instance, the
|
||||||
|
task tree calls the collectStorage handler, to enable reading the final data
|
||||||
|
from the storage, unique to this particular run of taskTree.
|
||||||
|
|
||||||
|
\section1 Task Adapters
|
||||||
|
|
||||||
|
To extend a TaskTree with new a 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 TimeoutAdapter : public Utils::Tasking::TaskAdapter<QTimer>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TimeoutAdapter() {
|
||||||
|
task()->setSingleShot(true);
|
||||||
|
task()->setInterval(1000);
|
||||||
|
connect(task(), &QTimer::timeout, this, [this] { emit done(true); });
|
||||||
|
}
|
||||||
|
void start() final { task()->start(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
QTC_DECLARE_CUSTOM_TASK(Timeout, TimeoutAdapter);
|
||||||
|
\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 TimeoutAdapter initially configures the QTimer object and connects
|
||||||
|
to the QTimer::timeout signal. When the signal is triggered, TimeoutAdapter
|
||||||
|
emits the done(true) signal to inform the task tree that the task finished
|
||||||
|
successfully. If it emits done(false), the task finished with an error.
|
||||||
|
The TaskAdapter::start() method starts the timer.
|
||||||
|
|
||||||
|
To make QTimer accessible inside TaskTree under the \e Timeout name,
|
||||||
|
register it with QTC_DECLARE_CUSTOM_TASK(Timeout, TimeoutAdapter). Timeout
|
||||||
|
becomes a new task type inside Utils::Tasking namespace, using TimeoutAdapter.
|
||||||
|
|
||||||
|
The new task type is now registered, and you can use it in TaskTree:
|
||||||
|
|
||||||
|
\code
|
||||||
|
const auto onTimeoutSetup = [](QTimer &task) {
|
||||||
|
task.setInterval(2000);
|
||||||
|
};
|
||||||
|
const auto onTimeoutDone = [](const QTimer &task) {
|
||||||
|
qDebug() << "timeout triggered";
|
||||||
|
};
|
||||||
|
|
||||||
|
const Group root {
|
||||||
|
Timeout(onTimeoutSetup, onTimeoutDone)
|
||||||
|
};
|
||||||
|
\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).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
TaskTree::TaskTree()
|
TaskTree::TaskTree()
|
||||||
|
|||||||
Reference in New Issue
Block a user