☰
如何快速看懂 Ely Prism Launcher 的任务框架:Task、SequentialTask 与 ConcurrentTask 设计剖析
2026/10/16 1:22:52 网站建设 项目流程

【免费下载链接】Launcher

This fork of Prism Launcher adds integrated support for Ely.by accounts.

项目地址:https://gitcode.com/GitHub_Trending/ely/Launcher
点击查看免费下载

Ely Prism Launcher 是一款支持 Ely.by 账号登录的 Minecraft 启动器(Prism Launcher 的 fork),它把"下载游戏文件、安装 Mod、检查 Java 环境"等复杂操作都交给一套统一的异步任务框架来驱动。本文带你逐层剖析launcher/tasks/目录下的三个核心类:Task、ConcurrentTask与SequentialTask,帮你理解启动器界面里那些进度条和"取消"按钮背后是如何工作的。

一、为什么启动器需要一套任务框架?

想象一下你点击"创建实例"按钮后发生的事:

  1. 先下载版本清单(JSON 文件)
  2. 再下载游戏本体 jar 包
  3. 同时并行下载几十个库文件和资源包
  4. 中途用户可能点击"取消"
  5. 界面要实时显示"正在执行第 3/10 步"

如果用一堆嵌套回调或死循环轮询来实现,代码会非常难维护。Ely Prism Launcher 的解法是:把一切耗时操作都抽象成"任务(Task)",任务有统一的状态、进度、日志和取消机制,还能像积木一样组合。

🧱 整套框架只有 6 个文件,位于 launcher/tasks/ 目录:

文件职责
Task.h / Task.cpp任务基类,定义状态机与信号
ConcurrentTask.h并发任务容器,最多同时跑 N 个子任务
SequentialTask.h顺序任务,并发数固定为 1
MultipleOptionsTask.h备选任务,逐个尝试直到成功

二、Task:任务框架的最小积木

Task是所有任务的基类(见 Task.h),它同时继承 Qt 的QObject和QRunnable——前者提供信号槽机制,后者让它可以在独立线程中运行。

任务的五种状态

Task用一个枚举描述了任务的一生:

enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
  • Inactive:刚创建,尚未开始
  • Running:正在执行
  • Succeeded / Failed:以成功或失败结束
  • AbortedByUser:被用户取消

外部调用start()启动任务,随后进入子类的executeTask()(纯虚函数)。任务结束时必须手动调用emitSucceeded()或emitFailed()报告结果(见 Task.h),这两个方法内部还做了"不能重复结束"的防错检查。

进度与日志:界面更新的唯一通道

Task提供了三组"上报"接口,UI 只需要监听信号即可:

接口对应信号界面效果
setStatus()status显示"正在下载 xxx"
setProgress(current, total)progress驱动进度条
setDetails()details显示详细备注

此外还有abort()/setAbortable()一组取消机制(Task.h):任务自己声明是否允许取消,界面上的"取消"按钮文本甚至可以由任务动态设置。

💡给新手的记忆点:一个合格的子任务类 = 实现executeTask()+ 在结束前调用emitSucceeded()/emitFailed()+ 过程中随时setStatus()/setProgress()。

三、ConcurrentTask:带"并发上限"的任务队列

如果说Task是单个工人,那ConcurrentTask(ConcurrentTask.h)就是工头——它把多个子任务放进队列,按max_concurrent(默认 6 个)的上限并行执行。

内部就是四个"桶" + 一个队列

QQueue<Task::Ptr> m_queue; // 排队等待 QHash<Task*, Task::Ptr> m_doing; // 正在执行 QHash<Task*, Task::Ptr> m_done; // 已完成 QHash<Task*, Task::Ptr> m_failed; // 失败

核心调度逻辑在executeNextSubTask()(ConcurrentTask.cpp):

  • 正在执行的任务数没达到上限 → 从队列取一个启动
  • 队列空且没有任务在跑 → 结算:全部成功则emitSucceeded();有失败的则汇总所有失败原因后emitFailed()

每个子任务完成时会触发subTaskFinished(),更新统计桶并再调一次调度,形成"完成一个、补上一个"的流水线。

失败聚合与进度透传

  • 多个子任务失败时,会把每份失败原因拼成一段文本一起报出,方便排查(ConcurrentTask.cpp)。
  • 子任务的 status / details / progress 信号会被ConcurrentTask转成统一的stepProgress信号向上传递,因此界面上能画出"每个步骤一行"的多步骤进度列表,这正是TaskStepProgressList数据结构(Task.h)的用途。

优雅取消

ConcurrentTask::abort()(ConcurrentTask.cpp)的做法很直观:清空待执行队列 + 逐个向正在跑的子任务喊abort();如果有任何一个子任务拒绝取消,整体按失败处理并提示"Failed to abort all running tasks."。

四、SequentialTask:并发数锁定为 1 的特例

SequentialTask的类声明只有短短几行(SequentialTask.h):

class SequentialTask : public ConcurrentTask { explicit SequentialTask(QString task_name = ""); void subTaskFailed(Task::Ptr, const QString& msg) override; void updateState() override; };

它在构造函数里直接调用ConcurrentTask(task_name, 1)——把并发上限设为 1,于是队列天然变成严格串行:前一个子任务成功结束后才启动下一个。

它与父类的行为差异只有两处(SequentialTask.cpp):

  1. 一个失败立即整体失败:覆写subTaskFailed(),在记录后立刻emitFailed(msg),不再等待剩余任务。因为顺序任务里后续步骤依赖前面的产出,继续跑没有意义。
  2. 状态文案更友好:显示"Executing task 3 out of 10"(执行第 3/10 步),而不是并发版的"同时执行 N 个"。

源码注释里有一句话很传神:"A concurrent task that only allows one concurrent task :)"—— 它本质上就是"只允许 1 个并发的并发任务"。

如何选择?一张表看懂四类任务

任务类并发度失败策略典型语义
Task1(自己跑)自己决定单一原子操作
ConcurrentTaskmax_concurrent(默认 6)聚合所有失败原因批量下载等可并行的活儿
SequentialTask1失败立即终止有依赖关系的步骤流水线
MultipleOptionsTask1全失败才失败逐个尝试的备选方案(见 MultipleOptionsTask.cpp)

五、实战:这套框架在启动器里如何落地

场景 1:创建实例时串行下载游戏文件

InstanceCreationTask.cpp 中,创建实例的最后一步组装了一个SequentialTask:先MinecraftLoadAndCheck(在线校验版本清单),再依次追加各组件更新任务,然后start()。注意它调用了propagateFromOther(task.get())——这是Task提供的一个便利方法(Task.cpp),能把子组合任务的进度、状态原样"透传"给自己的监听者,调用方因此完全不用关心组合细节。

场景 2:网络下载是"默认并发度"的主力用户

NetJob.h 让NetJob : public ConcurrentTask,把每个网络请求包装成子任务批量执行;并发上限来自用户设置项NumberOfConcurrentTasks(默认 10,见 Application.cpp)。Java 检测(JavaInstallList.cpp)和版本列表加载(VersionList.cpp)等场景也各自复用了ConcurrentTask/SequentialTask。

场景 3:启动游戏的 LaunchTask

游戏启动流程由 LaunchTask.h 实现,它本身是一个Task,但内部把"检查 Java → 解析服务器地址 → 启动进程"拆成一串LaunchStep顺序执行,并在失败时覆写emitFailed()做特殊处理——展示了框架允许在子类里定制信号行为的灵活性。

六、这套设计的可取之处

  1. 信号即契约:UI 与任务之间只靠 Qt 信号通信,界面线程永远不会被阻塞,这也解释了为什么进度条即使在下载几百 MB 时也丝滑流畅。
  2. 继承即编排:SequentialTask/MultipleOptionsTask几乎零代码,仅靠覆写一两个虚函数就改变了组合语义,是"以继承表达策略"的好例子。
  3. 防错优先:重复start()、非运行态下结束任务等异常路径都有断言与日志保护(Task.cpp);单元测试 tests/Task_test.cpp 里甚至专门构造了上万个子任务压测栈深度。
  4. 可取消是一等公民:从canAbort()到"取消按钮文案可定制",取消体验贯穿整个框架。

📌小结:Ely Prism Launcher 的任务框架用最小的Task状态机 +ConcurrentTask队列调度,派生出顺序执行与备选重试两种编排方式。理解这三个类,你就掌握了启动器几乎所有异步行为(下载、安装、启动、导出)的骨架,也学到了一个可迁移到任何 GUI 项目的异步任务设计范式。

【免费下载链接】Launcher

This fork of Prism Launcher adds integrated support for Ely.by accounts.

项目地址:https://gitcode.com/GitHub_Trending/ely/Launcher
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询