Worker API:让Gradle真正跑起来

说实话,很多Java开发者用Gradle好几年,都没意识到一个关键问题——你的构建任务,其实一直在「单线程」里打转。我刚开始带团队时也踩过这个坑,项目一大了,构建时间从几十秒飙到几分钟,大家干等着。后来我仔细研究了Worker API,才明白原来Gradle早就给了我们一把利器,只是很多人没用上。

什么是Worker API?

Worker API,说白了就是Gradle提供的一套并行任务执行框架。它允许你把一个任务拆成多个独立的工作单元,然后让Gradle自动调度这些单元在多个线程里跑。

你想想看,如果你的项目有10个模块需要编译,每个模块之间没有依赖关系,那为什么不让它们同时编译呢?Worker API就是干这个的。

核心思想:将任务拆分为可并行执行的「工作单元」(Work Unit),由Gradle的Worker Daemon统一调度。

为什么需要Worker API?

我见过太多人用Gradle的parallel属性,以为开了并行就万事大吉。其实parallel只是让不同任务并行,但一个任务内部如果有多步操作,它还是串行的。Worker API解决的是「任务内部的并行」问题。

举个例子:

  • 你有一个代码生成任务,需要生成100个文件
  • 每个文件的生成逻辑完全独立
  • 用普通方式:循环100次,一次生成一个
  • 用Worker API:拆成100个工作单元,同时跑

效果很明显——时间从100个单位降到10个单位(假设10个线程)。

Worker API的核心组件

要理解Worker API,你得先认识这几个角色:

组件 作用
WorkQueue 工作队列,你把任务提交到这里
WorkAction 实际干活的人,定义每个工作单元做什么
WorkParameters 干活需要的参数,比如输入文件、输出目录
WorkerExecutor 调度器,管理WorkQueue和Worker Daemon

它们的关系是这样的:

Worker API 核心架构 WorkerExecutor WorkQueue WorkAction #1 参数: 文件A WorkAction #2 参数: 文件B WorkAction #3 参数: 文件C Worker Daemon(线程池管理) 每个 WorkAction 在独立线程中执行,互不干扰

实战:写一个并行代码生成器

光说不练假把式。我直接给你看一个我在项目中用过的例子——一个代码生成任务,需要生成多个Java文件。

先定义参数:

interface CodeGenParams extends WorkParameters {
    // 要生成的类名
    Property<String> getClassName();
    // 输出目录
    DirectoryProperty getOutputDir();
    // 模板内容
    Property<String> getTemplate();
}

然后定义干活的人:

abstract class CodeGenAction implements WorkAction<CodeGenParams> {
    @Override
    public void execute() {
        String className = getParameters().getClassName().get();
        File outputDir = getParameters().getOutputDir().getAsFile().get();
        String template = getParameters().getTemplate().get();
        
        // 生成Java文件
        String content = template.replace("${CLASS_NAME}", className);
        File outputFile = new File(outputDir, className + ".java");
        outputFile.getParentFile().mkdirs();
        Files.write(outputFile.toPath(), content.getBytes());
        
        // 我习惯在这里加个日志,方便排查
        System.out.println("生成文件: " + outputFile.getAbsolutePath());
    }
}

最后在任务里提交:

abstract class CodeGenTask extends DefaultTask {
    @Inject
    abstract WorkerExecutor getWorkerExecutor();
    
    @Input
    List<String> classNames = ["UserService", "OrderService", "ProductService"];
    
    @OutputDirectory
    DirectoryProperty outputDir = ...;
    
    @TaskAction
    void generate() {
        // 创建一个WorkQueue,允许并行
        WorkQueue queue = getWorkerExecutor()
            .noIsolation()
            .processIsolation(settings -> {
                settings.forkOptions(jvmArgs -> {
                    jvmArgs.add("-Xmx256m");
                });
            });
        
        for (String name : classNames) {
            queue.submit(CodeGenAction.class, params -> {
                params.getClassName().set(name);
                params.getOutputDir().set(outputDir);
                params.getTemplate().set(getTemplate());
            });
        }
        
        // 等待所有任务完成
        queue.await();
    }
}

我的经验:这里有个细节——noIsolation()processIsolation()的区别。前者在同一个JVM里跑,速度快但隔离性差;后者会fork新进程,安全但开销大。我个人建议:如果任务不涉及类加载冲突,用noIsolation()就够了。

隔离与线程安全

说到隔离,我得提醒你一个坑。Worker API虽然帮你管理了线程,但它不保证你的代码是线程安全的。

我曾经遇到过一个案例:两个WorkAction同时往同一个文件里写内容,结果文件内容乱掉了。排查了半天,才发现是共享了同一个输出流。

避坑指南:

  • 每个WorkAction的输入参数必须是不可变的,或者至少是线程安全的
  • 输出文件路径要确保唯一,不要两个Action写同一个文件
  • 不要在WorkAction里访问外部的可变状态,比如全局变量
  • 如果必须共享资源,用synchronized或者ReentrantLock保护

Gradle提供了三种隔离级别:

隔离级别 说明 适用场景
noIsolation() 在同一个JVM里运行,共享类加载器 简单任务,无类冲突风险
classLoaderIsolation() 同一个JVM,但使用独立的类加载器 需要隔离类库的任务
processIsolation() fork新进程,完全隔离 需要独立JVM参数或极度安全

性能提升案例

说个真实数据。我之前维护的一个微服务项目,有12个模块。每次构建时,有一个「代码检查」任务,需要对每个模块运行PMD和Checkstyle。

改造前:

  • 串行执行12个模块的检查
  • 每个模块耗时约8秒
  • 总耗时:96秒

改造后(用Worker API,8个线程):

  • 并行执行12个模块的检查
  • 每个模块耗时约8秒
  • 总耗时:16秒(8个模块同时跑,剩下4个再跑一轮)

提升了整整6倍。你想想看,如果每次构建省下80秒,一天构建10次就是800秒,一个月就是6个多小时。这效率提升,谁用谁知道。

关键配置:别忘了在gradle.properties里设置org.gradle.workers.max=8,否则Worker API默认只开4个线程。

什么时候不该用Worker API?

嗯,这里要注意。Worker API不是万能药。我见过有人滥用它,反而让构建变慢了。

  • 任务本身很快:如果每个工作单元执行时间不到100ms,线程调度的开销可能超过执行本身
  • IO密集型任务:如果任务主要是在读写磁盘,并行反而可能造成IO争抢
  • 有严格顺序依赖:如果任务B必须等任务A的结果,那就别强行并行

说白了,Worker API最适合的是CPU密集型的、相互独立的任务。比如代码生成、静态分析、单元测试(如果测试之间无依赖)等。

最后说一句,Worker API是Gradle 4.0引入的,到现在已经很成熟了。如果你还在用老版本的Gradle,建议升级。我见过太多团队守着旧版本,错过了这么好的性能优化手段。


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