4. 并行任务组:beginWith传入多个WorkRequest、then与并行合并的实际效果

4.1 核心原理:并行入口与串联出口

在WorkManager的链式调度中,beginWith()是构建任务链的唯一入口方法。当传入多个WorkRequest时,这些任务会被系统安排为并行执行,且只有所有并行任务均成功完成后,后续的then()任务才会被触发。这种“并行入口 + 串联出口”的组合机制是构建高效异步工作流的基础。

调用方式行为描述典型场景
beginWith(singleWork)单一任务启动,顺序执行then()链线性依赖步骤
beginWith(listOfWork)多任务并行启动,所有任务完成后继续then()并行数据下载、并行预处理
beginWith() + then() + then()并行入口之后串联任意数量的顺序任务并行下载→统一解析→入库

4.2 实现流程:真正的并行合并是怎样工作的

假设需要完成“并行下载图片A和图片B → 合并生成缩略图 → 上传结果”三个步骤。WorkManager通过以下内部机制保证并行与合并的正确执行:

  1. 任务图构建:beginWith()会创建一个WorkContinuation对象,内部维护一个DAG(有向无环图)。
  2. 并行调度:传入的多个WorkRequest被添加为同层节点,没有先后依赖关系,WorkManager线程池允许同时执行。
  3. 合并等待:then()在添加后续任务时,会自动将所有前驱节点的父级依赖合并。即后续任务需要等待前驱节点的“所有并行任务”都完成后才进入执行队列。
  4. 并行+串联组合:可以在任意位置使用then()插入顺序任务,也可以在then()之后再次使用beginWith()类型的合并(通过WorkContinuation.combine())。
提示:并行任务组中所有任务共享同一个ListenableWorker的生命周期约束。如果其中任何一个任务返回Result.failure(),整个continuation会被标记为失败,且后续then()任务不会执行。

4.3 代码示范:beginWith多任务与then串联

以下是一个完整的Kotlin示例,展示如何创建三个并行Worker,然后串联一个合并Worker:

// 1. 定义三个并行Worker(模拟图片下载、数据预处理、缓存清理)
val downloadWorker = OneTimeWorkRequestBuilder<DownloadWorker>()
    .setInputData(workDataOf("url" to "https://example.com/a.jpg"))
    .build()

val preprocessWorker = OneTimeWorkRequestBuilder<PreprocessWorker>()
    .setInputData(workDataOf("mode" to "thumbnail"))
    .build()

val cacheCleanWorker = OneTimeWorkRequestBuilder<CacheCleanWorker>()
    .build()

// 2. 定义合并处理Worker(仅在上述三个都成功后执行)
val mergeWorker = OneTimeWorkRequestBuilder<MergeWorker>()
    .build()

// 3. 构建并行入口
val workContinuation = WorkManager.getInstance(context)
    .beginWith(listOf(downloadWorker, preprocessWorker, cacheCleanWorker))
    .then(mergeWorker)

// 4. 观察合并前后的状态
workContinuation.enqueue()

// 5. (可选) 进一步串联后续任务
val uploadWorker = OneTimeWorkRequestBuilder<UploadWorker>().build()
workContinuation.then(uploadWorker).enqueue()

4.4 实际效果:并行与串行的可视化对比

下方SVG流程图直观展示了beginWith(三个并行任务) → then(一个合并任务)的执行顺序:

并行入口 beginWith() + then() 执行流程 任务A: 下载图片 任务B: 预处理数据 任务C: 清理缓存 并行 顺序 then: 合并处理 then: 上传 ✅ 三个并行任务同时执行;仅当全部成功(Result.success()),合并任务才会被调度。 ❌ 任何一个失败或取消,合并任务与后续任务均不执行(异常路径已忽略)。

4.5 调试与日志观察

在调试模式下,可以通过WorkManager的WorkContinuation.debug()方法观察任务图的依赖关系。推荐使用以下ADB命令过滤并行任务状态:

adb logcat -s WorkManager:V
# 或过滤所有Worker状态
adb logcat | grep -E "WorkProgress|WorkerStatus|beginWith|then"

典型日志输出会显示三个并行任务几乎同时被,并分别以状态执行。仅当所有并行任务转换为后,合并任务才会进入队列。

4.6 常见错误与避坑指南

  • 任务过多导致线程池满:WorkManager默认线程池大小为最大1个CPU核心+1。如果并行任务超过16个,可能造成死等待。建议根据设备性能将并行任务数量控制在8-12个以内。
  • 依赖不清晰:beginWith传入的任务之间如果有隐式数据依赖,必须使用then()显式串联,否则并行会导致竞态条件。
  • 结果合并失败:使用combine()进行多个Continuation的合并时,务必确保每个分支都正确调用了.enqueue(),否则合并永远不会触发。
  • 错误的使用then后再次beginWith:在一个Continuation上连续调用beginWith不会重置任务图,会抛出IllegalStateException。应使用WorkContinuation.combine()实现分支合并。

4.7 性能优化建议

场景建议原因
并行任务组很大(>10)拆分为多个小并行组,使用then串联避免线程池资源竞争导致的延迟抖增
并行任务存在IO密集型在Worker内部使用自定义线程池WorkManager默认线程池偏重CPU任务
需要合并多个并行结果使用combine()而不是手动等待保持WorkManager生命周期管理的一致性
优化验证:使用WorkManager.getWorkInfoByIdLiveData()观察每个Worker的执行时长。如果并行组中多个Worker的总执行时间超过单个Worker时长+20%,则说明线程池争用严重,应该降低并行度或启用keepResultsForAtLeast持久化策略。