Worker API与并行执行:让Gradle真正跑起来
说实话,很多团队用Gradle构建Java项目,最头疼的就是构建速度。尤其是那种几十个模块的大项目,跑一次全量构建,喝杯咖啡回来还没完。我早期带的一个项目就是这样,CI流水线动不动就超时,后来我花了整整一周时间,把构建时间从12分钟压缩到了3分钟。核心武器是什么?就是今天要讲的Worker API和并行执行。
Worker API是什么?
Worker API是Gradle提供的一套底层机制。它的作用很简单:让你写的任务能够安全地并行执行。
你想想看,Gradle本身是支持并行构建的,比如--parallel参数。但如果你自己写了一个自定义任务,里面操作了文件、调用了外部进程,那这个任务能不能并行跑?默认情况下,Gradle会给你加锁,保证同一时刻只有一个实例在执行。这其实是一种保护,但也是性能瓶颈。
Worker API就是用来打破这个瓶颈的。它允许你定义一种“工作单元”,然后让Gradle的Worker进程池去调度执行。每个Worker运行在独立的进程中,互不干扰。
核心概念:
- WorkAction:你定义的具体工作逻辑,实现一个接口
- WorkParameters:传递给WorkAction的参数,必须是可序列化的
- WorkerExecutor:服务接口,用来提交工作项
- 进程隔离:每个Worker运行在独立的Gradle Worker进程中
一个简单的例子:并行处理文件
假设我们要写一个任务,把一堆Java源文件中的版权头信息替换掉。如果文件很多,串行处理会很慢。用Worker API可以轻松并行化。
先定义参数和动作:
// 定义参数
abstract class UpdateCopyrightParams implements WorkParameters {
abstract Property<File> getSourceFile()
abstract Property<String> getNewHeader()
}
// 定义工作动作
abstract class UpdateCopyrightAction implements WorkAction<UpdateCopyrightParams> {
@Override
public void execute() {
def file = parameters.sourceFile.get()
def header = parameters.newHeader.get()
def content = file.text
// 替换版权信息
file.text = header + "\n" + content
logger.lifecycle("已更新: {}", file.name)
}
}
然后在任务中使用WorkerExecutor提交:
abstract class UpdateCopyrightTask extends DefaultTask {
@InputFiles
abstract ConfigurableFileCollection getSourceFiles()
@Input
abstract Property<String> getNewHeader()
@Inject
abstract WorkerExecutor getWorkerExecutor()
@TaskAction
void updateFiles() {
def executor = workerExecutor
sourceFiles.each { file ->
executor.submit(UpdateCopyrightAction) { config ->
config.sourceFile = file
config.newHeader = newHeader
}
}
}
}
嗯,这里要注意:@Inject注解用来获取WorkerExecutor,这是Gradle推荐的依赖注入方式。我刚开始用的时候,总想着自己new一个出来,结果死活拿不到,后来看了文档才明白。
并行任务执行:不只是--parallel
很多人以为Gradle的并行执行就是加个--parallel参数。其实远不止如此。Worker API让你在任务内部实现并行,这是更细粒度的控制。
举个例子:你有一个任务需要编译100个协议缓冲区(Protobuf)文件。如果只用--parallel,Gradle会并行执行不同的任务,但你这个任务内部还是串行的。用Worker API,你可以把这100个文件的编译工作拆成100个工作项,让Worker池并行处理。
我的经验:我曾经在一个项目中,把Protobuf编译任务从串行改成了Worker API并行。原来编译一次需要40秒,改成8个Worker并行后,直接降到了8秒。效果立竿见影。
隔离与线程安全:为什么Worker API更安全
你可能会问:我自己用Java的线程池不也能并行吗?为什么非要用Worker API?
原因在于隔离性。Worker API的每个工作单元运行在独立的进程中。这意味着:
- 一个Worker崩溃了,不会影响其他Worker
- Worker之间没有共享内存,不存在线程安全问题
- 每个Worker有自己的类加载器,不会出现类冲突
- Gradle可以回收Worker进程,避免内存泄漏
我自己用线程池踩过坑。有一次写了一个并行处理图片压缩的任务,结果多个线程同时写同一个临时目录,文件互相覆盖,最后生成的图片全是花的。改用Worker API后,每个Worker有自己的临时目录,问题迎刃而解。
避坑指南:我曾经犯过一个错误——在WorkAction中使用了全局静态变量。虽然Worker是进程隔离的,但如果你在WorkAction中引用了外部的单例对象,而这个对象没有被正确序列化,就会导致运行时异常。记住:WorkAction中只能使用WorkParameters传递的数据。
Worker API的配置与调优
Worker API不是银弹。用不好反而会拖慢构建。这里有几个关键配置:
| 参数 | 说明 | 建议值 |
|---|---|---|
maxWorkerCount |
最大Worker进程数 | CPU核心数 - 1 |
forkMode |
进程复用策略 | ALWAYS(总是新建)或 ONCE(复用) |
classLoaderCache |
是否缓存类加载器 | 建议开启,减少类加载开销 |
我个人习惯把maxWorkerCount设置为CPU核心数减1。为什么留一个?因为Gradle本身也需要CPU资源,全占满会导致系统卡顿。当然,如果你的机器是专门跑构建的,可以设置为等于核心数。
知识体系图
下面这张图展示了Worker API在整个Gradle并行执行体系中的位置:
什么时候该用Worker API?
不是所有场景都适合用Worker API。我总结了几条经验:
- 任务内部有大量可拆分的独立工作:比如处理100个文件、编译50个模块
- 工作单元之间没有依赖:如果A的结果是B的输入,那就不能并行
- 工作单元耗时较长:每个工作单元至少几百毫秒,否则进程创建的开销会抵消并行收益
- 需要进程隔离:比如调用不安全的第三方库,或者操作全局资源
一句话总结:Worker API让你在任务内部实现安全、高效的并行执行。它比线程池更安全,比--parallel更细粒度。用好它,你的Gradle构建速度能再上一个台阶。
好了,关于Worker API和并行执行,我就讲这么多。记住:构建速度的提升,往往不是靠某一个技巧,而是靠多个优化点的叠加。Worker API是其中一个非常强大的工具,值得你花时间去掌握。
公众号:蓝海资料掘金营,微信deep3321