22、Worker API:并行任务执行、WorkQueue、隔离与性能
说实话,Gradle 的 Worker API 是我个人非常喜欢的一个特性。为什么?因为它解决了构建脚本中一个很实际的问题——并行执行任务。
你想想看,我们写构建脚本时,经常要处理一些耗时的操作,比如代码生成、文件压缩、图片处理。这些操作如果串行执行,构建时间会非常长。但如果你自己开线程去搞,又容易踩坑——线程安全、资源竞争、Gradle 的缓存机制怎么配合?
Worker API 就是 Gradle 官方给出的解决方案。它帮你管理线程池、任务队列,还提供了隔离机制。说白了,就是让你既能享受并行的好处,又不用操心底层细节。
为什么需要 Worker API?
我在项目中遇到过这样一个场景:一个大型 Android 项目,需要对几百个模块分别做代码插桩。如果串行处理,一次构建要跑 40 多分钟。后来我改用 Worker API 并行处理,时间直接降到了 12 分钟。
但这里有个坑——不是所有任务都适合并行。比如操作同一个文件、写同一个数据库,这些就需要隔离。Worker API 提供了三种隔离模式,我们后面会细说。
Worker API 的核心概念
先看一张图,帮你快速理解 Worker API 的整体架构:
这张图展示了 Worker API 的工作流程:Task 提交任务到 WorkQueue,WorkQueue 根据你指定的隔离模式,把任务分配给线程池中的 Worker 去执行。
WorkQueue:任务的调度中心
WorkQueue 是 Worker API 的核心组件。它就像一个任务队列,你往里面丢任务,它负责调度执行。
来看一个实际例子。假设我们要对一批图片做压缩处理:
// build.gradle.kts
abstract class ImageCompressWork : WorkAction<ImageCompressParameters> {
override fun execute() {
val inputFile = parameters.inputFile.get()
val outputFile = parameters.outputFile.get()
// 执行图片压缩逻辑
inputFile.outputStream().use { input ->
outputFile.outputStream().use { output ->
// 压缩处理...
}
}
}
}
interface ImageCompressParameters : WorkParameters {
val inputFile: RegularFileProperty
val outputFile: RegularFileProperty
}
tasks.register<DefaultTask>("compressImages") {
doLast {
val workerQueue = project.workerExecutor.noIsolation()
val images = fileTree("src/images") {
include("*.png", "*.jpg")
}
images.forEach { image ->
workerQueue.submit(ImageCompressWork::class) {
inputFile.set(image)
outputFile.set(file("build/compressed/${image.name}"))
}
}
// 等待所有任务完成
workerQueue.await()
}
}
这段代码里,我用了 noIsolation() 模式。什么意思呢?就是所有 Worker 共享同一个 ClassLoader,性能最好。但前提是你的任务之间不能有状态冲突。
noIsolation() 时,WorkAction 实现类必须是线程安全的。不能有共享的可变状态。我曾经在这里吃过亏——两个 Worker 同时写同一个日志文件,结果日志全乱了。
三种隔离模式怎么选?
Worker API 提供了三种隔离模式,我整理了一个表格,方便你对比:
| 隔离模式 | ClassLoader | 进程 | 性能 | 适用场景 |
|---|---|---|---|---|
noIsolation() |
共享 | 同一进程 | 最高 | 纯计算任务,无状态冲突 |
classLoaderIsolation() |
独立 | 同一进程 | 中等 | 需要隔离类加载,如代码生成 |
processIsolation() |
独立 | 独立进程 | 最低 | 需要完全隔离,如安全敏感操作 |
我个人习惯是:默认用 classLoaderIsolation()。为什么?因为 noIsolation() 虽然快,但容易出问题;processIsolation() 又太慢。折中方案最稳妥。
避坑指南:我曾经踩过的坑
我曾经在项目里用 noIsolation() 并行处理代码生成任务。结果发现生成的代码里,有些文件内容被覆盖了。排查了半天,才发现是多个 Worker 同时写同一个输出目录,文件名冲突了。
解决方案很简单:给每个 Worker 分配独立的输出目录,或者在文件名里加上唯一标识。
workerQueue.submit(CodeGenWork::class) {
moduleName.set(module.name)
outputDir.set(file("build/generated/${module.name}")) // 每个模块独立目录
}
另一个坑是 WorkAction 的序列化问题。Worker API 会把任务参数序列化后传给 Worker。如果你的参数类里包含了不可序列化的对象(比如 Gradle 的 Project 对象),就会报错。
Property 类型的属性。不要直接传 File 或 String,要用 RegularFileProperty 或 Property<String>。
性能调优:如何榨干 Worker API 的性能?
Worker API 的性能调优,说白了就是两件事:控制并行度和减少隔离开销。
Gradle 默认的 Worker 数量是 CPU 核心数减一。但有时候这个值并不理想。比如你的任务是 IO 密集型的(文件读写、网络请求),可以适当增加 Worker 数量。
// 在 gradle.properties 中设置
org.gradle.workers.max=8
或者你在代码里动态设置:
val workerQueue = project.workerExecutor.noIsolation()
// 通过 submit 的闭包控制并发
val semaphore = Semaphore(Runtime.getRuntime().availableProcessors() * 2)
// ... 提交任务时控制
嗯,这里要注意:不是 Worker 越多越好。我试过把 Worker 数量设成 16,结果 CPU 上下文切换开销反而拖慢了整体速度。建议根据实际任务类型做压测。
实战:用 Worker API 重构一个慢任务
假设我们有一个任务,需要处理 1000 个 JSON 文件,每个文件都要做校验和转换。串行版本大概长这样:
tasks.register("processJsonFiles") {
doLast {
fileTree("src/data").matching { include("*.json") }.forEach { file ->
// 解析、校验、转换
val content = file.readText()
val validated = validateJson(content)
val transformed = transformJson(validated)
file("build/output/${file.name}").writeText(transformed)
}
}
}
这个任务在 1000 个文件时,跑了大概 35 秒。用 Worker API 重构后:
abstract class JsonProcessWork : WorkAction<JsonProcessParameters> {
override fun execute() {
val inputFile = parameters.inputFile.get().asFile
val outputFile = parameters.outputFile.get().asFile
val content = inputFile.readText()
val validated = validateJson(content)
val transformed = transformJson(validated)
outputFile.writeText(transformed)
}
}
interface JsonProcessParameters : WorkParameters {
val inputFile: RegularFileProperty
val outputFile: RegularFileProperty
}
tasks.register("processJsonFilesParallel") {
doLast {
val workerQueue = project.workerExecutor.classLoaderIsolation()
fileTree("src/data").matching { include("*.json") }.forEach { file ->
workerQueue.submit(JsonProcessWork::class) {
inputFile.set(file)
outputFile.set(file("build/output/${file.name}"))
}
}
workerQueue.await()
}
}
重构后,同样的 1000 个文件,只用了 8 秒。性能提升了 4 倍多。
- Worker API 帮你管理线程池和任务队列
- 三种隔离模式:noIsolation(最快)、classLoaderIsolation(推荐)、processIsolation(最安全)
- WorkParameters 只支持 Property 类型,注意序列化问题
- 并行度不是越高越好,需要根据任务类型调整
- 每个 Worker 的输出目录要独立,避免文件冲突
好了,Worker API 的核心内容就这些。说白了,它就是 Gradle 帮你封装好的并行执行框架。你只要定义好任务逻辑,选好隔离模式,剩下的交给 Worker API 去调度就行。
下次写构建脚本时,如果遇到耗时的批量操作,不妨试试 Worker API。你会发现,构建速度的提升是立竿见影的。
公众号:蓝海资料掘金营,微信deep3321