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并行执行体系中的位置:

Gradle并行执行体系 Gradle构建 --parallel 任务级并行 内置任务并行 Worker API 任务A、任务B、任务C 各自独立执行,互不干扰 WorkAction + WorkParameters 进程隔离,安全并行 依赖关系管理、增量构建 进程隔离、线程安全、可配置

什么时候该用Worker API?

不是所有场景都适合用Worker API。我总结了几条经验:

  1. 任务内部有大量可拆分的独立工作:比如处理100个文件、编译50个模块
  2. 工作单元之间没有依赖:如果A的结果是B的输入,那就不能并行
  3. 工作单元耗时较长:每个工作单元至少几百毫秒,否则进程创建的开销会抵消并行收益
  4. 需要进程隔离:比如调用不安全的第三方库,或者操作全局资源

一句话总结:Worker API让你在任务内部实现安全、高效的并行执行。它比线程池更安全,比--parallel更细粒度。用好它,你的Gradle构建速度能再上一个台阶。

好了,关于Worker API和并行执行,我就讲这么多。记住:构建速度的提升,往往不是靠某一个技巧,而是靠多个优化点的叠加。Worker API是其中一个非常强大的工具,值得你花时间去掌握。


公众号:蓝海资料掘金营,微信deep3321