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 提交 Worker WorkQueue 隔离模式 Gradle 管理的线程池(自动分配 Worker) Worker A | Worker B | Worker C | ... CLASSLOADER 隔离 每个 Worker 独立 ClassLoader NONE 隔离 共享 ClassLoader,性能最高 PROCESS 隔离 独立进程,最安全但最慢 性能:NONE > CLASSLOADER > PROCESS | 安全性:PROCESS > CLASSLOADER > NONE

这张图展示了 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 对象),就会报错。

💡 小技巧: WorkParameters 接口只支持 Property 类型的属性。不要直接传 FileString,要用 RegularFilePropertyProperty<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