全新的 Qt Task Tree 模块即将作为技术预览版随 Qt 6.11 版本发布,是一个管理自动化异步任务的全面解决方案。它最特别的一点是,在 Qt C++ API 设计上采用了全新的方法,改变了我们编写和读取异步代码 (asynchronous code)的思维方式。此外,它还统一了各种异步 API(asynchronous APIs),提供了让任意异步任务(asynchronous task)适配本模块的方法 。
Qt Task Tree 演示示例截图。
Qt 以其用 QML 语言表达的声明式 API 而闻名。新的 Qt Task Tree 模块将这种声明式 API 的理念带到了 Qt 的 C++ 世界 。
异步工作流(asynchronous workflow)的声明式描述是通过所谓的配方(recipe)来表达的。配方(recipe)是一个可复制的值类型对象,会被传递给 Task Tree 实例。当 Task Tree 启动时,它会读取 recipe,并根据传递的配方自动管理复杂的异步工作流。工作流包括动态创建异步任务、创建用于任务间数据交换的数据对象、已创建任务的生命周期管理以及在任务完成后执行后续操作。
Recipe 是可重复使用的——这意味着它们可以作为通用配方的一部分,可以多次运行,或由多个并行运行的 Task Tree 来运行。
可以将配方(recipe)和 Task Tree 比作卡带和卡带播放器。卡带本身不会执行任何操作。同样,在定义配方时,您不会创建任何任务或数据对象。当配方传递给 Task Tree 并启动 Task Tree 时,您只是定义了 Task Tree 应该做的具体描述。这正如将卡带插入播放器并开始播放时发生的情形——播放器读取卡带内容后,便能精确执行相应操作。
当您想创建一个新游戏时,您只需设计一个新的游戏卡带,然后让玩家根据卡带的内容来执行它。卡带设计者不需要改变播放器中的任何内容,只需在插入卡带后按下“播放“/”取消“/”重置“按钮即可。配方(recipes)也是一样:若想编写自己的复杂异步工作流,只需准备专属配方。随后将其传入 Task Tree ,即可对 Task Tree 进行播放/取消/重置操作。无需对 Task Tree 本身做任何修改就能正确执行配方。
这种代码编写思维的转变,是成功使用此模块的关键因素。
作为 Qt 首个声明式 C++ API,了解如何使用它以及区分配方(recipe)和 Task Tree 的确切职责至关重要。
Qt Task Tree 模块的用户重点是创建一个精确描述所需异步工作流程的配方。 配方的内容完全是声明性的,重要的是要记住它只是一个描述,仅此而已。
另一件需要记住的重要事情是,Task Tree 本身就是一个配方阅读器/执行器。使用 Task Tree 很简单--您不需要(也不应该)创建 QTaskTree 子类,只需将配方传递给它并运行即可。然后,您可以取消/重置它,或接收完成通知,仅此而已。
这就是 Task Tree 为您带来的最大好处--在 Task Tree 的引擎盖下,Task Tree 为您完成了大量枯燥的重复工作,而无需额外编码。
让我们来看看配方和 Task Tree 的具体职责,以及配方内容如何影响正在运行的 Task Tree :
|
配方(卡带),由用户定义并描述: |
QTaskTree(播放器),由用户实例化,读取传递的配方并自动执行以下操作: |
| 需要执行哪些任务 | 创建和销毁任务,负责管理其生命周期 |
| 执行顺序 | 遵守配方定义的任务执行顺序 |
| 执行模式(顺序、并行) | 按顺序或并行执行任务 |
| 工作流程策略 | 根据已完成任务的结果和已定义的工作流程策略,通过继续执行、跳过剩余任务和取消并行运行的任务来控制执行 |
| 嵌套组 | 执行一组子任务,其父任务将其视为单个异步任务,拥有自己的执行模式和工作流策略 |
| 设置处理程序 | 在任务或组启动前执行设置处理程序,以便根据用户需求配置任务 |
| 完成处理程序 | 在任务或组结束时执行完成处理程序,以便从已完成的任务中收集结果数据 |
| 用于任务间数据交换的Storage<DataType> 对象 | 动态创建和销毁由相应 Storage<DataType> 对象描述的 DataType 型对象,用于不同任务和组的设置和完成处理程序或条件表达式中 |
| 条件表达式 | 根据收集的数据或异步任务结果选择不同的执行路径 |
此外,QTaskTree 还提供有关已完成任务的基本进度信息,并能取消所有当前正在运行的任务,不论当前的执行状态如何。从开发人员的角度来看,唯一的工作就是定义配方(表格左列)。在表格的右栏,您可以看到QTaskTree 为您完成多少工作,无需要任何额外代码。这几乎消除了异步编程中通常需要的所有冗余代码。
从模板代码中明确分离出确切的工作流程,使代码比传统方法更具可读性。Task Tree 为您处理了繁琐的任务,模板代码便不复存在。 另一方面,即使工作流程再复杂,也能在单一位置清晰、准确地描述出来。
让我们来看一个简单的示例配方。
配方由顶层的 Group 元素组成。在这个元素中,我们定义了属性和任务。请注意下面的解释中使用了 "when this recipe is run" (当此配方运行时)这一短语,强调事情不是在定义配方时发生的,而是在配方执行期间发生的。
第一个属性值是 sequential,这意味着当运行此配方时,顶层组的直接子任务将以链式方式执行,即前一个任务完成后,下一个任务才开始执行。顺序元素是任何组的默认执行模式,可以省略。
第二个属性值是 stopOnError。这意味着在运行此配方时,只要顶层组的任何直接子任务出错退出,我们就会停止执行此组,跳过所有待定任务,并报告错误。否则,在所有任务都成功完成后,我们就会报告执行成功。这也是任何组的默认工作流策略,因此也可以省略。
接下来,我们定义顶层 Group 的第一个任务 QNetworkReplyWrapperTask,该任务将在运行此配方时异步检索网络数据。
顶层组的最后一个元素是嵌套 Group。 运行此配方时,它将在下载任务成功完成后执行。该组包含 Parallel 属性,确保所有任务并行执行。最后,我们定义了两个 QConcurrentCallTask<QImage> 任务,每个任务都会在运行此配方时执行一个在单独线程中运行的函数,将收集到的网络数据转换为所需大小的 QImage。
最后一步是将配方传递给 QTaskTree 实例并运行它。
Qt Task Tree 模块解决了异步编程的一个重要方面,将各种异步 API 统一为一个的接口。
如果没有这种统一,要自动创建、启动、通知任务完成和删除不同类型的任务将非常困难。 下表显示了不同的异步 Qt API 如何启动任务。创建、完成和销毁 API 也存在类似的差异。
|
异步任务 |
启动 API |
| 进程 | QProcess::start() |
| 网络查询 | QNetworkAccessManager::get()/put()/post()/other… Instantiates QNetworkReply dynamically. |
| 在独立线程中运行 | QtConcurrent::run() Returns QFuture handle |
| 定时器 | QTimer::singleShot() |
在 Qt Task Tree 模块中,异步 API 的不兼容性问题已得到解决。所有这类任务类型都通过统一接口实现了标准化,该接口利用为现有 API 提供的任务适配器实现。有关内置适配器以及如何适配任何其他自定义异步任务类型的更多信息,请参阅 QTaskInterface 和 QCustomTask 文档。
Qt Task Tree 模块最初是在 2022 年底作为 QtCreator 项目的一部分开发的。在此期间,我们增加了许多改进,仅举几例:
Qt Task Tree 最终发展成熟,在 Qt 6.11 中作为公共 API 发布。目前, QtCreator 中已有超过 100 处功能采用 Qt Task Tree 进行管理,包括管理构建/部署/运行项目配置、控制所有定位器过滤器的执行、执行 VCS 命令、驱动 Axivion 插件网络通信、执行 Clang 工具、运行自动测试,等等.……
Qt Task Tree 模块包含几个演示其功能的示例。让我们简单了解一下:
演示示例展示了不同的执行模式和工作流策略如何影响复杂 Task Tree 结构中子任务的执行。它还允许您观察任务持续时间及其预期结果如何影响执行的持续性。该示例也有网络组件版本,因此您可以直接在浏览器中使用。
Traffic Light 示例演示了如何将 Task Tree 用作状态机。它展示了如何使用 Forever 元素来构建可作为状态机运行的重复配方。
使用 Qt Task Tree ,可以同时启动所有异步迭代。图像缩放示例展示了这一点。它还解释了如何使用存储对象(Storage object)在不同任务之间交换动态数据。
在这里,您可以找到完整文档、source tree 和包含其他信息的通用 Task Tree 页面的链接。
我们很高兴看到 Qt 6.11的官方公开版本发布了这个新模块,这标志着 Qt 首个声明式 C++ API 的诞生。这一API 设计的创新让我们不禁好奇:开发者将如何体验这种编程思维的转变。我们期待听到您的反馈。祝您探索全新的 Qt Task Tree 模块愉快!