Qt TaskTree C++ Classes
包含一个通用的 TaskTree 库。更多
本模块正在开发中,可能会有所更改。
此模块在 Qt 6.11 中引入。
命名空间
包含 TaskTree 模块的所有类和全局函数 |
类
与 For 和 When 结构一起使用的主体元素 | |
条件表达式中使用的 "else "元素 | |
条件表达式中使用的 "else if "元素 | |
可执行任务项的基类 | |
描述执行模式的组元素 | |
for 循环元素 | |
子任务的无限循环 | |
在 For 元素中使用的无限迭代器 | |
代表组成声明式配方的基本元素,描述如何执行和处理嵌套的异步任务树 | |
代表可作为任何组的一部分的基本元素 | |
条件表达式中使用的 "if "元素 | |
在 For 元素中用作迭代器的基类 | |
用于 For 元素内部的列表迭代器 | |
描述 QObject 子类及其信号的结构 | |
具有自定义限制的并行执行模式 | |
按需完成的异步任务 | |
用于声明自定义任务项并定义其设置和完成处理程序的类模板 | |
一个提供 QCustomTask 中使用的默认任务适配器的类模板 | |
具有给定关键字类型的映射任务树执行控制器 | |
QNetworkReply 和 QNetworkAccessManager 的包装器 | |
并行任务树执行控制器 | |
QProcessTask 使用的 QProcess 自定义删除程序 | |
顺序任务树执行控制器 | |
单任务树执行控制器 | |
具有给定限制的启动 QBarrier | |
在其他任务之间同步执行自定义处理程序 | |
在调整自定义任务接口时使用的辅助类 | |
运行以声明方式定义的异步任务树 | |
QTcpSocket 的包装器 | |
通过 QtConcurrent::run() 在独立线程中控制函数执行的类模板 | |
QThreadFunction 类模板的基类 | |
在 For 元素中使用的重复迭代器 | |
运行任务树中用于自定义数据交换的类模板 | |
条件表达式中使用的 "then "元素 | |
用于 For 元素的条件迭代器 | |
延迟执行主体直到障碍提前的元素 |
详细说明
使用 TaskTree 库构建配方,描述要执行的异步任务,并在 QTaskTree 中使用这些配方来执行这些任务。
这些配方是对要创建和执行的任务类型的声明性描述,例如QProcess,QNetworkReplyWrapper, 或QThreadFunction<ReturnType>,或者这些任务是按顺序运行还是并行运行。在配方中,你可以根据前一个任务是成功完成还是出错,定义不同的继续路径。也可以在Group 元素中嵌套任务,每个Group 可以根据自己的执行模式或工作流策略运行任务。配方构成了任务树结构。
异步任务
异步任务是指任何可以启动,随后以成功或错误结束的任务。稍后是指在启动任务后,控制权回到运行中的事件循环。在任务完成之前,我们不会阻塞调用线程。为了使用任务树,我们需要一个事件循环。
异步任务示例:
- QTimer::singleShot()
- QProcess
- QNetworkAccessManager +QNetworkReply =QNetworkReplyWrapper
- QtConcurrent::run()+QFutureWatcher<Result> =QThreadFunction<Result>
配方和任务树
为了记住什么是配方和任务树,让我们用弹夹和播放器来做个类比。当我们编写配方时,就好像我们在制作一个弹夹,所以我们只需准备一份详细描述,说明当弹夹放入播放器(任务树)并启动后,播放器应该做什么。配方本身只是任务树的声明性描述,说明当配方传递给任务树并启动任务树时,任务树应该做什么。在没有任务树的情况下,配方本身不会做任何事情,就像没有播放器的卡匣一样。
以下是有关配方和任务树职责的简短总结。
配方(盒式磁带)描述:
- 运行中的任务树(通过 QCustomTask)将动态创建哪些任务
- 以何种顺序创建
- 运行中的任务树(通过Storage )将动态创建哪些数据结构
- 如何在启动前设置每个任务
- 任务完成后如何收集数据
- 执行模式(任务应按顺序运行还是并行运行)
- 工作流程策略
任务树(播放器):
- 读取配方并自动创建任务和数据结构
- 管理已创建任务和数据结构的生命周期
- 执行连续任务
- 根据已完成任务的结果和工作流策略选择不同路径
- 提供基本的进度信息
自定义任务
由于配方是对任务树在启动时应创建哪些任务的描述,我们无法直接在配方中创建这些任务。相反,我们需要一种声明式方法来告诉任务树在稍后的时间点为我们创建和启动这些任务。例如,如果我们想让任务树创建并启动QProcess ,我们可以通过在配方中放置QProcessTask 元素来描述它。QProcessTask 是QCustomTask<QProcess> 的别名。每个任务类型都应提供相应的QCustomTask<Type>,以便在配方中使用。
下表列出了一些内置的自定义任务,这些任务可以放在配方中:
| 自定义任务(在配方中使用) | 任务类(由运行中的任务树创建) | 简要说明 |
|---|---|---|
| QProcessTask | QProcess | 启动进程。 |
QThreadFunctionTask<ReturnType> | QThreadFunction<ReturnType> | 启动异步任务,在独立线程中运行。 |
| QTaskTreeTask | QTaskTree | 启动嵌套任务树。 |
| QNetworkReplyWrapperTask | QNetworkReplyWrapper | 启动网络下载。 |
| QTcpSocketWrapperTask | QTcpSocketWrapper | 启动 TCP 连接。 |
有关如何调整特定任务以便在配方中使用的更多信息,请参阅QTaskInterface 和Task Adapters 。
配方示例
QTaskTree 有一个顶层的Group 元素,又称配方,它可以包含任意数量的各种类型的任务,如QProcessTask,QNetworkReplyWrapperTask, 或QThreadFunctionTask<ReturnType>:
const Group recipe { QProcessTask(...), QNetworkReplyWrapperTask(...), QThreadFunctionTask<int>(...) }; QTaskTree *taskTree = new QTaskTree(recipe); connect(taskTree, &QTaskTree::done, ...); // finish handler taskTree->start();
上述配方由Group 类型的顶层元素组成,其中包含QProcessTask 、QNetworkReplyWrapperTask 和QThreadFunctionTask<int> 类型的任务。调用taskTree->start() 后,创建任务并以链式运行,从QProcess 开始。当QProcess 成功完成后,就会启动QNetworkReplyWrapper 任务。最后,当网络任务成功完成后,启动QThreadFunction<int> 任务。
当最后一个正在运行的任务成功结束时,任务树被认为已成功运行,并发出QTaskTree::done() 信号DoneWith::Success 。当某个任务以错误方式结束时,任务树的执行将停止,并跳过剩余的任务。任务树结束时出现错误,会发出QTaskTree::done() 信号,DoneWith::Error 。
组
Group 的父任务将其视为单个任务。与其他任务一样,组可以启动,也可以在成功或出错的情况下结束。Group 元素可以嵌套,形成树形结构:
const Group recipe { Group { parallel, QProcessTask(...), QThreadFunctionTask<int>(...) }, QNetworkReplyWrapperTask(...) };
上述示例与第一个示例的不同之处在于,顶层元素有一个子组,其中包含QProcessTask 和QThreadFunctionTask<int>。子组是根元素QNetworkReplyWrapperTask 的同级元素。子组包含一个额外的parallel 元素,指示其Group 并行执行任务。
因此,当QTaskTree 启动上述配方时,QProcess 和QThreadFunction<int> 会立即启动并行运行。由于根组不包含parallel 元素,其直接子任务会按顺序运行。因此,当整个子组结束时,QNetworkReplyWrapper 开始运行。当组的所有任务都完成后,该组才算完成。任务完成的顺序无关紧要。
因此,根据哪个任务持续时间更长(QProcess 或QThreadFunction<int>),可能会出现以下情况:
| 情况 1 | 方案 2 |
|---|---|
| 根组启动 | 根组启动 |
| 子组启动 | 子组启动 |
| QProcess 开始 | QProcess 开始 |
| QThreadFunction<int> starts | QThreadFunction<int> starts |
| ... | ... |
| QProcess 结束 | QThreadFunction<int> 结束 |
| ... | ... |
| QThreadFunction<int> 完成 | QProcess 子组 |
| 子组完成 | 子组完成 |
| QNetworkReplyWrapper 启动 | QNetworkReplyWrapper 启动 |
| ... | ... |
| QNetworkReplyWrapper 结束 | QNetworkReplyWrapper 结束 |
| 根组结束 | 根组完成 |
方案之间的差异用粗体标出。三个圆点表示上一个事件和下一个事件(一个或多个任务继续运行)之间有一段未指定的时间间隔。事件之间没有点表示它们同步发生。
所提供的情景假设所有任务都成功运行。如果任务在执行过程中失败,任务树会以错误结束。特别是,当QProcess 出错而QThreadFunction<int> 仍在执行时,QThreadFunction<int> 会自动取消,子组出错,QNetworkReplyWrapper 跳过,任务树出错结束。
任务处理程序
使用Task 处理程序来设置任务的执行,并在任务执行成功或出错时读取任务的输出数据。
任务启动处理程序
创建任务对象后,在启动之前,任务树会调用一个用户提供的可选设置处理程序。设置处理程序应始终引用相关的任务类对象:
const auto onSetup = [](QProcess &process) { process.setProgram("sleep"); process.setArguments({"3"}); }; const Group root { QProcessTask(onSetup) };
你可以在设置处理程序中修改传入的QProcess ,这样任务树就能根据你的配置启动进程。您不应在设置处理程序中调用process.start(); ,因为任务树会在需要时调用它。设置处理程序是可选的。使用时,它必须是任务构造函数的第一个参数。
作为可选项,设置处理程序可返回SetupResult 。返回的SetupResult 会影响给定任务的进一步启动行为。可能的值有
| SetupResult 值 | 简要说明 |
|---|---|
| Continue | 任务将正常启动。这是设置处理程序未返回SetupResult (即返回类型为void )时的默认行为。 |
| StopWithSuccess | 任务不会启动,但会向其父级报告成功。 |
| StopWithError | 任务不会启动,但会向上级报告错误。 |
这对于仅在满足某一条件时运行任务非常有用,而评估该条件所需的数据要等到先前启动的任务完成后才能知道。这样,设置处理程序就能动态决定是正常启动相应任务,还是跳过该任务并报告成功或错误。有关任务间数据交换的更多信息,请参阅Storage 。
任务完成处理程序
当一个正在运行的任务结束时,任务树会调用一个可选提供的完成处理程序。该处理程序应使用相关任务类对象的const 引用:
const autoonSetup= [](QProcess&process) { process.setProgram("sleep"); process.setArguments({"3"}); };const autoonDone= [](constQProcess&process,DoneWith result) {if(result==DoneWith::Success) qDebug() << "Success" << process.cleanedStdOut(); 不然 qDebug() << "Failure" << process.cleanedStdErr(); };constGroup root {QProcessTask(onSetup,onDone) };
完成处理程序可收集来自QProcess 的输出数据,并将其存储起来以作进一步处理或执行其他操作。
注意: 如果任务设置处理程序返回StopWithSuccess 或StopWithError ,则不会调用已完成处理程序。
组处理程序
与任务处理程序类似,组处理程序可让您设置一个组来执行,并在整个组执行成功或出错时应用更多操作。
组的启动处理程序
任务树在启动子任务前会调用组启动处理程序。组处理程序不需要任何参数:
const autoonSetup= []{ qDebug() << "Entering the group"; };ConstGroup root { onGroupSetup(onSetup), QProcessTask(...) };
组设置处理程序是可选的。要定义组设置处理程序,请在组中添加onGroupSetup() 元素。onGroupSetup() 的参数是用户处理程序。如果在组中添加一个以上的onGroupSetup() 元素,运行时会触发一个包含错误信息的断言。
与任务的启动处理程序一样,组启动处理程序也可能返回SetupResult 。返回的SetupResult 值会影响整个组的启动行为。如果未指定组启动处理程序,或其返回类型为 void,则组的默认操作是Continue ,这样所有任务都会正常启动。否则,当启动处理程序返回StopWithSuccess 或StopWithError 时,任务不会启动(会被跳过),组本身会根据返回值分别报告成功或错误。
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 };
在上例中,根组的所有子组都定义了各自的设置处理程序。下面的方案假定所有启动的进程都成功完成:
| 方案 | 注释 |
|---|---|
| 根组启动 | 不返回 SetupResult,因此其任务已执行。 |
| 组 1 启动 | 返回 Continue,因此其任务被执行。 |
| 进程 1 启动 | |
| ... | ... |
| 进程 1 结束(成功) | |
| 组 1 结束(成功) | |
| 第 2 组启动 | 返回 StopWithSuccess,因此进程 2 被跳过,组 2 报告成功。 |
| 第 2 组结束(成功) | |
| 第 3 组启动 | 返回 StopWithError,因此进程 3 被跳过,组 3 报告错误。 |
| 第 3 组结束(错误) | |
| 根分组结束(错误) | 组 3 是根组的直接子组,它在结束时出错,因此根组停止执行,跳过尚未启动的进程 4 并报错。 |
组的完成处理程序
Group 的完成处理程序在任务执行成功或失败后执行。组报告的最终值取决于Workflow Policy 。处理程序还可以应用其他必要的操作。完成处理程序是在组的onGroupDone() 元素中定义的。它可以接受可选的DoneWith 参数,表示执行成功或失败:
constGroup root { onGroupSetup([]{qDebug()<< "Root setup"; }), QProcessTask(...),onGroupDone([](DoneWith result) {if(result==DoneWith::Success) qDebug() << "Root finished with success"; 不然 qDebug() << "Root finished with an error"; }) };
组完成处理程序是可选的。如果向一个组添加一个以上的onGroupDone() ,运行时会触发一个包含错误信息的断言。
注意: 即使组设置处理程序返回StopWithSuccess 或StopWithError ,也会调用组的完成处理程序。这种行为不同于任务已完成处理程序,将来可能会改变。
其他组元素
组可以包含描述处理流程的其他元素,如execution mode 或workflow policy 。组还可以包含负责收集和共享在组执行过程中收集到的自定义公共数据的存储元素。
执行模式
组中的执行模式元素指定了组的直接子任务的启动方式。最常见的执行模式是sequential 和parallel 。还可以使用ParallelLimit 元素指定并行运行任务的限制。
在所有执行模式下,组都会按照任务出现的顺序启动任务。
如果组的子组也是一个组,则子组按照自己的执行模式运行任务。
工作流程策略
Group 中的工作流策略元素规定了组在其任何直接子任务完成时的行为方式。有关可能策略的详细说明,请参阅WorkflowPolicy 。
如果组的子组也是一个组,则子组按照自己的工作流策略运行任务。
存储
使用Storage 元素在任务之间交换信息。特别是在顺序执行模式下,当一个任务需要另一个已完成任务的数据时,才可以启动。例如,一个任务树通过从一个源文件读取数据并将其写入一个目标文件来复制数据,该任务树可能如下所示:
静态QByteArrayload(constQString文件名) {...}static voidsave(constQString文件名, 常量QByteArray&array) {...}staticGroup copyRecipe(constQString源, 常量QString结构CopyStorage {// [1] 自定义任务间结构 QByteArraycontent;// [2] 自定义任务间数据};// [3] 可由任务树管理的自定义任务间结构体实例 const Storage<CopyStorage>storage;const autoonLoaderSetup= [source](QThreadFunction<QByteArray> &async) { async.setThreadFunctionData(&load,source); };// [4] 运行时:任务树在调用处理程序之前激活 [7] 中的实例 const autoonLoaderDone= [storage](const QThreadFunction<QByteArray> &async) { storage->content =async.result();// [5] 加载器将结果存储在存储器中};// [4] 运行时:任务树在调用处理程序之前激活 [7] 中的实例 const autoonSaverSetup= [storage,destination](QThreadFunction<void> &async) {constQByteArraycontent= storage->content;// [6] saver 从存储中获取数据async.setThreadFunctionData(&save,destination,content); };const autoonSaverDone= [](const QThreadFunction<void> &async) { qDebug() << "Save done successfully"; };constGroup root {// [7] runtime: task tree creates an instance of CopyStorage when root is enteredstorage, QThreadFunctionTask<voidQByteArray>(onLoaderSetup,onLoaderDone,CallDoneFlag::OnSuccess), QThreadFunctionTask<void>(onSaverSetup,onSaverDone,CallDoneFlag::OnSuccess) };returnroot; }...constQStringsource= ...;constQStringdestination= ...;QTaskTree taskTree(copyRecipe(source,destination)); connect(&taskTree, &QTaskTree::done, &taskTree, [](DoneWith result) {if(result==DoneWith::Success) qDebug() << "The copying finished successfully."; }); tasktree.start();
在上例中,任务间数据由QByteArray 内容变量 [2] 和CopyStorage 自定义结构 [1] 组成。如果加载程序成功完成,它就会将数据存储到CopyStorage::content 变量 [5]。然后,保存器使用该变量配置保存任务 [6]。
为使任务树能管理CopyStorage 结构,创建了Storage<CopyStorage> 的实例 [3]。如果将该对象的副本作为组的子项插入[7],那么当任务树进入该组时,就会动态创建CopyStorage 结构的实例。当任务树离开该组时,由于不再需要CopyStorage 结构体的现有实例,因此会将其销毁。
StorageCopyStorage如果多个任务树同时运行(包括任务树在不同线程中运行的情况),则每个任务树都包含自己的CopyStorage 结构体副本。
您可以通过存储对象从组中的任何处理程序访问CopyStorage 。这包括具有存储对象的组中所有子任务的所有处理程序。要访问处理程序中的自定义结构体,请将Storage<CopyStorage> 对象的副本传递给处理程序(例如,在 lambda 捕捉中)[4]。
当任务树调用包含存储[7]的子树中的处理程序时,任务树会激活Storage<CopyStorage> 对象内自己的CopyStorage 实例。因此,只能从处理程序主体中访问CopyStorage 结构。要从Storage<CopyStorage> 访问当前激活的CopyStorage ,请使用Storage::operator->()、Storage::operator*() 或Storage::activeStorage() 方法。
以下列表总结了如何在任务树中使用存储对象:
- 使用自定义数据定义自定义结构
MyStorage[1]、[2] - 创建Storage<
MyStorage> 存储 [3] 的实例 - 将Storage<
MyStorage> 实例传递给处理程序 [4]. - 在处理程序中访问
MyStorage实例 [5],[6] - 将Storage<
MyStorage> 实例插入组 [7].
QTaskTree 类
QTaskTree 根据Group 根元素描述的配方执行异步任务树结构。
由于QTaskTree 也是异步任务,它可以成为另一个QTaskTree 的一部分。要将嵌套的QTaskTree 放在另一个QTaskTree 中,请将QTaskTreeTask 元素插入另一个Group 元素中。
QTaskTree 运行时报告已完成任务的进度。当任务完成、跳过或取消时,进度值会增加。当QTaskTree 完成并发出QTaskTree::done() 信号时,进度的当前值等于最大进度值。最大进度值等于树中异步任务的总数。嵌套的QTaskTree 计为单个任务,其子任务不计入顶层树中。QSyncTask 任务不是异步任务,因此不计入任务。
要为运行中的树设置额外的初始数据,可在创建树时通过安装存储设置处理程序来修改树中的存储实例:
Storage<CopyStorage> storage; const Group root = ...; // storage placed inside root's group and inside handlers QTaskTree taskTree(root); auto initStorage = [](CopyStorage &storage) { storage.content = "initial content"; }; taskTree.onStorageSetup(storage, initStorage); taskTree.start();
当运行中的任务树创建CopyStorage 实例时,在树内的任何处理程序被调用之前,任务树会调用initStorage 处理程序,以便设置存储的初始数据,这是taskTree 的特定运行所独有的。
同样,要从运行中的任务树中收集一些额外的结果数据,可在任务树中的存储实例即将销毁时读取这些数据。为此,请安装一个存储已完成处理程序:
Storage<CopyStorage>storage;constGroup root= ...;// 存储放在 root 的组内和处理程序内QTaskTree taskTree(root);autocollectStorage= [](constCopyStorage&storage) { qDebug() << "final content" << storage.content; }; taskTree.onStorageDone(storage,collectStorage); taskTree.start();
当运行中的任务树即将销毁CopyStorage 实例时,任务树会调用 collectStorage 处理程序,以便从存储中读取该特定运行taskTree 所独有的最终数据。
任务适配器
允许新任务类型成为配方的一部分非常简单。只需在QCustomTask 模板中定义一个新任务别名,将Task 类型作为第一个模板参数传递即可,例如
class Worker : public QObject { public: void start() { ... } signals: void done(bool result); }; using WorkerTask = QCustomTask<Worker>;
如果满足以下条件,这个任务就会生效:
- 您的任务源于QObject 。
- 您的任务有启动任务的公共 start() 方法。
- 任务完成时发出 done(bool) 或 done(DoneResult) 信号。
如果您的任务不符合这些条件,您仍然可以通过提供自定义适配器的第二个模板参数来调整您的任务,使其与 TaskTree 框架协同工作。比方说,我们想让QTimer 与 TaskTree 配合使用。Adapter 可以是这样的
class TimerAdapter { public: void operator()(QTimer *task, QTaskInterface *iface) { task->setSingleShot(true); QObject::connect(task, &QTimer::timeout, iface, [iface] { iface->reportDone(DoneResult::Success); }); task->start(); } }; using TimerTask = QCustomTask<QTimer, TimerAdapter>;
现在,您可以开始在您的食谱中使用TimerTask ,例如
const autoonSetup= [](QTimer任务) { task.setInterval(2000); };const autoonDone= [](constQTimer任务){ qDebug() << "Timer triggered after" << task.interval() << "ms."; };constGroup recipe { TimerTask(onSetup,onDone) };
注: 实现运行任务的类应有一个默认构造函数,该类的对象应可自由销毁。应允许销毁运行中的任务,最好是不等待运行中的任务完成(即运行中任务的安全非阻塞销毁器)。要实现对具有阻塞析构函数的任务的非阻塞析构,可考虑使用QCustomTask 的可选Deleter 模板参数(第三个模板参数)。
任务树运行程序
任务树运行程序管理用于执行给定配方的底层QTaskTree 的生命周期。
下表总结了各种任务树运行程序之间的区别:
| 类名 | 描述 |
|---|---|
| QSingleTaskTreeRunner | 管理单个任务树的执行。QSingleTaskTreeRunner::start() 方法无条件启动传递的配方,重置任何可能正在运行的任务树。一次只能执行一个任务树。 |
| QSequentialTaskTreeRunner | 管理任务树的顺序执行。如果任务树运行程序处于空闲状态,QSequentialTaskTreeRunner::enqueue() 方法会启动传递的配方。否则,该配方将被挂起。当当前任务结束时,运行程序会按顺序执行被取消的配方。一次只能执行一个任务树。 |
| QParallelTaskTreeRunner | 管理并行的任务树执行。QParallelTaskTreeRunner::start() 方法会无条件启动传递的配方,并以并行方式保留任何可能正在运行的任务树。 |
| QMappedTaskTreeRunner | 管理映射任务树执行。QMappedTaskTreeRunner::start() 方法无条件启动指定键的指定配方。如果已经运行了具有相同密钥的不同任务树,它将被重置。不同键的任务树不受影响,会继续执行。 |
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.