Gradle 提供了一个 API,可以将任务拆分为可并行执行的部分。

writing tasks 5

这使 Gradle 能够充分利用可用资源并更快地完成构建。

Worker API

Worker API 提供了将任务操作的执行分解为离散的工作单元,然后并发且异步地执行该工作的能力。

Worker API 示例

了解如何使用 API 的最佳方法是完成将现有自定义任务转换为使用 Worker API 的过程

  1. 您将首先创建一个自定义任务类,为一组可配置的文件生成 MD5 哈希。

  2. 然后,您将转换此自定义任务以使用 Worker API。

  3. 接下来,我们将探讨使用不同隔离级别运行任务。

在此过程中,您将了解 Worker API 的基础知识及其提供的功能。

步骤 1. 创建自定义任务类

首先,创建一个自定义任务,生成一组可配置文件的 MD5 哈希值。

在新的目录中,创建一个 buildSrc/build.gradle(.kts) 文件

buildSrc/build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    implementation("commons-io:commons-io:2.5")
    implementation("commons-codec:commons-codec:1.9") (1)
}
buildSrc/build.gradle
repositories {
    mavenCentral()
}

dependencies {
    implementation 'commons-io:commons-io:2.5'
    implementation 'commons-codec:commons-codec:1.9' (1)
}
1 您的自定义任务类将使用 Apache Commons Codec 生成 MD5 哈希值。

接下来,在 buildSrc/src/main/java 目录中创建一个自定义任务类。您应该将此类命名为 CreateMD5

buildSrc/src/main/java/CreateMD5.java
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkerExecutor;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

abstract public class CreateMD5 extends SourceTask { (1)

    @OutputDirectory
    abstract public DirectoryProperty getDestinationDirectory(); (2)

    @TaskAction
    public void createHashes() {
        for (File sourceFile : getSource().getFiles()) { (3)
            try {
                InputStream stream = new FileInputStream(sourceFile);
                System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
                // Artificially make this task slower.
                Thread.sleep(3000); (4)
                Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");  (5)
                FileUtils.writeStringToFile(md5File.get().getAsFile(), DigestUtils.md5Hex(stream), (String) null);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}
1 SourceTask 是针对对一组源文件进行操作的任务的便捷类型。
2 任务输出将进入配置的目录。
3 该任务会遍历定义为“源文件”的所有文件,并为每个文件创建一个 MD5 哈希值。
4 插入一段人为睡眠,以模拟对大文件进行哈希处理(示例文件不会那么大)。
5 每个文件的 MD5 哈希值都会写入输出目录中,写入的文件同名,但扩展名为“md5”。

接下来,创建一个 build.gradle(.kts),注册您的新 CreateMD5 任务

build.gradle.kts
plugins { id("base") } (1)

tasks.register<CreateMD5>("md5") {
    destinationDirectory = project.layout.buildDirectory.dir("md5") (2)
    source(project.layout.projectDirectory.file("src")) (3)
}
build.gradle
plugins { id 'base' } (1)

tasks.register("md5", CreateMD5) {
    destinationDirectory = project.layout.buildDirectory.dir("md5") (2)
    source(project.layout.projectDirectory.file('src')) (3)
}
1 应用 base 插件,这样您将拥有一个 clean 任务,可用于删除输出。
2 MD5 哈希文件将写入 build/md5
3 此任务将为 src 目录中的每个文件生成 MD5 哈希文件。

您需要一些源文件来生成 MD5 哈希值。在 src 目录中创建三个文件

src/einstein.txt
Intellectual growth should commence at birth and cease only at death.
src/feynman.txt
I was born not knowing and have had only a little time to change that here and there.
src/hawking.txt
Intelligence is the ability to adapt to change.

此时,您可以通过运行 ./gradlew md5 来测试您的任务

$ gradle md5

输出应类似于

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for hawking.txt...

BUILD SUCCESSFUL in 9s
3 actionable tasks: 3 executed

build/md5 目录中,您现在应该会看到带有 md5 扩展名的相应文件,其中包含来自 src 目录的文件的 MD5 哈希值。请注意,该任务至少需要 9 秒才能运行,因为它一次对每个文件进行哈希处理(即三个文件,每个文件大约需要 3 秒)。

步骤 2. 转换为 Worker API

虽然此任务按顺序处理每个文件,但每个文件的处理与任何其他文件无关。此项工作可以并行完成,并利用多个处理器。这就是 Worker API 可以提供帮助的地方。

要使用 Worker API,您需要定义一个接口,表示每个工作单元的参数,并扩展 org.gradle.workers.WorkParameters

对于 MD5 哈希文件生成,工作单元需要两个参数

  1. 要进行哈希处理的文件,以及

  2. 要写入哈希的文件。

无需创建具体实现,因为 Gradle 将在运行时为我们生成一个具体实现。

buildSrc/src/main/java/MD5WorkParameters.java
import org.gradle.api.file.RegularFileProperty;
import org.gradle.workers.WorkParameters;

public interface MD5WorkParameters extends WorkParameters {
    RegularFileProperty getSourceFile(); (1)
    RegularFileProperty getMD5File();
}
1 使用 Property 对象表示源文件和 MD5 哈希文件。

然后,你需要将自定义任务中为每个单独文件执行工作的部分重构到一个单独的类中。此类是你的“工作单元”实现,它应该是一个扩展 org.gradle.workers.WorkAction 的抽象类

buildSrc/src/main/java/GenerateMD5.java
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.gradle.workers.WorkAction;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public abstract class GenerateMD5 implements WorkAction<MD5WorkParameters> { (1)
    @Override
    public void execute() {
        try {
            File sourceFile = getParameters().getSourceFile().getAsFile().get();
            File md5File = getParameters().getMD5File().getAsFile().get();
            InputStream stream = new FileInputStream(sourceFile);
            System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
            // Artificially make this task slower.
            Thread.sleep(3000);
            FileUtils.writeStringToFile(md5File, DigestUtils.md5Hex(stream), (String) null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
1 不要实现 getParameters() 方法 - Gradle 将在运行时注入此方法。

现在,将你的自定义任务类更改为向 WorkerExecutor 提交工作,而不是自己执行工作。

buildSrc/src/main/java/CreateMD5.java
import org.gradle.api.Action;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.workers.*;
import org.gradle.api.file.DirectoryProperty;

import javax.inject.Inject;
import java.io.File;

abstract public class CreateMD5 extends SourceTask {

    @OutputDirectory
    abstract public DirectoryProperty getDestinationDirectory();

    @Inject
    abstract public WorkerExecutor getWorkerExecutor(); (1)

    @TaskAction
    public void createHashes() {
        WorkQueue workQueue = getWorkerExecutor().noIsolation(); (2)

        for (File sourceFile : getSource().getFiles()) {
            Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
            workQueue.submit(GenerateMD5.class, parameters -> { (3)
                parameters.getSourceFile().set(sourceFile);
                parameters.getMD5File().set(md5File);
            });
        }
    }
}
1 需要 WorkerExecutor 服务才能提交你的工作。创建一个带 javax.inject.Inject 注释的抽象 getter 方法,当任务创建时,Gradle 将在运行时注入服务。
2 在提交工作之前,使用所需的隔离模式(如下所述)获取一个 WorkQueue 对象。
3 在提交工作单元时,指定工作单元实现,在本例中为 GenerateMD5,并配置其参数。

此时,你应该能够重新运行你的任务

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for hawking.txt...

BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed

结果看起来应该和以前一样,尽管 MD5 哈希文件可能会以不同的顺序生成,因为工作单元是并行执行的。然而,这次任务运行得快多了。这是因为 Worker API 并行执行每个文件的 MD5 计算,而不是按顺序执行。

步骤 3. 更改隔离模式

隔离模式控制 Gradle 如何将工作项相互隔离以及与 Gradle 运行时的其他部分隔离。

WorkerExecutor 上有三个控制此隔离的方法

  1. noIsolation()

  2. classLoaderIsolation()

  3. processIsolation()

noIsolation() 模式是最低级别的隔离,它将阻止工作单元更改项目状态。这是最快的隔离模式,因为它设置和执行工作项所需的开销最小。但是,它将为所有工作单元使用一个共享类加载器。这意味着每个工作单元都可以通过静态类状态相互影响。这也意味着每个工作单元都使用构建脚本类路径上库的相同版本。如果你希望用户能够配置任务以使用 Apache Commons Codec 库的不同(但兼容)版本运行,则需要使用不同的隔离模式。

首先,您必须将 buildSrc/build.gradle 中的依赖项更改为 compileOnly。这会告诉 Gradle 在构建类时应使用此依赖项,但不要将其放在构建脚本类路径中

buildSrc/build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    implementation("commons-io:commons-io:2.5")
    compileOnly("commons-codec:commons-codec:1.9")
}
buildSrc/build.gradle
repositories {
    mavenCentral()
}

dependencies {
    implementation 'commons-io:commons-io:2.5'
    compileOnly 'commons-codec:commons-codec:1.9'
}

接下来,更改 CreateMD5 任务以允许用户配置他们想要使用的编解码器库的版本。它将在运行时解析库的适当版本,并配置工作进程以使用此版本。

classLoaderIsolation() 方法告诉 Gradle 在具有隔离类加载器的线程中运行此工作

buildSrc/src/main/java/CreateMD5.java
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.process.JavaForkOptions;
import org.gradle.workers.*;

import javax.inject.Inject;
import java.io.File;
import java.util.Set;

abstract public class CreateMD5 extends SourceTask {

    @InputFiles
    abstract public ConfigurableFileCollection getCodecClasspath(); (1)

    @OutputDirectory
    abstract public DirectoryProperty getDestinationDirectory();

    @Inject
    abstract public WorkerExecutor getWorkerExecutor();

    @TaskAction
    public void createHashes() {
        WorkQueue workQueue = getWorkerExecutor().classLoaderIsolation(workerSpec -> {
            workerSpec.getClasspath().from(getCodecClasspath()); (2)
        });

        for (File sourceFile : getSource().getFiles()) {
            Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
            workQueue.submit(GenerateMD5.class, parameters -> {
                parameters.getSourceFile().set(sourceFile);
                parameters.getMD5File().set(md5File);
            });
        }
    }
}
1 公开编解码器库类路径的输入属性。
2 在创建工作队列时,在 ClassLoaderWorkerSpec 上配置类路径。

接下来,您需要配置您的构建,以便它有一个存储库来在任务执行时查找编解码器版本。我们还创建一个依赖项,以从该存储库中解析我们的编解码器库

build.gradle.kts
plugins { id("base") }

repositories {
    mavenCentral() (1)
}

val codec = configurations.create("codec") { (2)
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
    }
    isVisible = false
    isCanBeConsumed = false
}

dependencies {
    codec("commons-codec:commons-codec:1.10") (3)
}

tasks.register<CreateMD5>("md5") {
    codecClasspath.from(codec) (4)
    destinationDirectory = project.layout.buildDirectory.dir("md5")
    source(project.layout.projectDirectory.file("src"))
}
build.gradle
plugins { id 'base' }

repositories {
    mavenCentral() (1)
}

configurations.create('codec') { (2)
    attributes {
        attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
    }
    visible = false
    canBeConsumed = false
}

dependencies {
    codec 'commons-codec:commons-codec:1.10' (3)
}

tasks.register('md5', CreateMD5) {
    codecClasspath.from(configurations.codec) (4)
    destinationDirectory = project.layout.buildDirectory.dir('md5')
    source(project.layout.projectDirectory.file('src'))
}
1 添加一个存储库来解析编解码器库 - 这可以是与用于构建 CreateMD5 任务类的存储库不同的存储库。
2 添加一个配置来解析我们的编解码器库版本。
3 配置 Apache Commons Codec 的备用兼容版本。
4 配置 md5 任务以使用该配置作为其类路径。请注意,该配置将在执行任务之前不会解析。

现在,如果您运行您的任务,它应该按预期使用配置的编解码器库版本工作

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for hawking.txt...

BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed

步骤 4. 创建工作进程守护程序

有时,在执行工作项时需要利用更高级别的隔离。例如,外部库可能依赖于设置某些系统属性,这可能在工作项之间产生冲突。或者,某个库可能与 Gradle 正在运行的 JDK 版本不兼容,可能需要使用不同版本运行它。

Worker API 可以使用 processIsolation() 方法来适应这种情况,该方法导致工作在单独的“工作进程守护程序”中执行。这些工作进程守护程序进程将在构建之间持续存在,并且可以在后续构建期间重复使用。但是,如果系统资源不足,Gradle 将停止未使用的工作进程守护程序。

要使用工作进程守护程序,请在创建 WorkQueue 时使用 processIsolation() 方法。您可能还希望为新进程配置自定义设置

buildSrc/src/main/java/CreateMD5.java
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.process.JavaForkOptions;
import org.gradle.workers.*;

import javax.inject.Inject;
import java.io.File;
import java.util.Set;

abstract public class CreateMD5 extends SourceTask {

    @InputFiles
    abstract public ConfigurableFileCollection getCodecClasspath(); (1)

    @OutputDirectory
    abstract public DirectoryProperty getDestinationDirectory();

    @Inject
    abstract public WorkerExecutor getWorkerExecutor();

    @TaskAction
    public void createHashes() {
        (1)
        WorkQueue workQueue = getWorkerExecutor().processIsolation(workerSpec -> {
            workerSpec.getClasspath().from(getCodecClasspath());
            workerSpec.forkOptions(options -> {
                options.setMaxHeapSize("64m"); (2)
            });
        });

        for (File sourceFile : getSource().getFiles()) {
            Provider<RegularFile> md5File = getDestinationDirectory().file(sourceFile.getName() + ".md5");
            workQueue.submit(GenerateMD5.class, parameters -> {
                parameters.getSourceFile().set(sourceFile);
                parameters.getMD5File().set(md5File);
            });
        }
    }
}
1 将隔离模式更改为 PROCESS
2 为新进程设置 JavaForkOptions

现在,您应该能够运行您的任务,它将按预期工作,但使用工作进程守护程序代替

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for hawking.txt...

BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed

请注意,执行时间可能很长。这是因为 Gradle 必须为每个工作程序守护进程启动一个新进程,而这代价很高。

但是,如果你第二次运行任务,你将看到它运行得快得多。这是因为在初始构建期间启动的工作程序守护进程已持久存在,并且在后续构建期间可立即使用

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for hawking.txt...

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

隔离模式

Gradle 提供了三种隔离模式,可在创建 WorkQueue 时进行配置,并使用 WorkerExecutor 上的以下方法之一指定

WorkerExecutor.noIsolation()

这表示应该在具有最小隔离性的线程中运行工作。
例如,它将共享加载任务的同一类加载器。这是最快的隔离级别。

WorkerExecutor.classLoaderIsolation()

这表示应该在具有隔离类加载器的线程中运行工作。
类加载器将具有从加载工作实现类的类加载器中的类路径以及通过 ClassLoaderWorkerSpec.getClasspath() 添加的任何其他类路径条目。

WorkerExecutor.processIsolation()

这表示应该通过在单独进程中执行工作来以最大隔离级别运行工作。
进程的类加载器将使用从加载工作单元的类加载器中的类路径以及通过 ClassLoaderWorkerSpec.getClasspath() 添加的任何其他类路径条目。此外,该进程将是一个工作程序守护进程,它将保持活动状态,并且可以重复用于具有相同要求的未来工作项。可以使用 ProcessWorkerSpec.forkOptions(org.gradle.api.Action) 使用不同于 Gradle JVM 的设置来配置此进程。

工作程序守护进程

在使用 processIsolation() 时,Gradle 将启动一个长寿命的工作程序守护进程,该进程可以重复用于未来的工作项。

build.gradle.kts
// Create a WorkQueue with process isolation
val workQueue = workerExecutor.processIsolation() {
    // Configure the options for the forked process
    forkOptions {
        maxHeapSize = "512m"
        systemProperty("org.gradle.sample.showFileSize", "true")
    }
}

// Create and submit a unit of work for each file
source.forEach { file ->
    workQueue.submit(ReverseFile::class) {
        fileToReverse = file
        destinationDir = outputDir
    }
}
build.gradle
// Create a WorkQueue with process isolation
WorkQueue workQueue = workerExecutor.processIsolation() { ProcessWorkerSpec spec ->
    // Configure the options for the forked process
    forkOptions { JavaForkOptions options ->
        options.maxHeapSize = "512m"
        options.systemProperty "org.gradle.sample.showFileSize", "true"
    }
}

// Create and submit a unit of work for each file
source.each { file ->
    workQueue.submit(ReverseFile.class) { ReverseParameters parameters ->
        parameters.fileToReverse = file
        parameters.destinationDir = outputDir
    }
}

当为工作程序守护进程提交一个工作单元时,Gradle 将首先查看是否存在一个兼容的空闲守护进程。如果存在,它将把工作单元发送到空闲守护进程,并将其标记为繁忙。如果不存在,它将启动一个新的守护进程。在评估兼容性时,Gradle 会查看多个条件,所有条件都可以通过 ProcessWorkerSpec.forkOptions(org.gradle.api.Action) 控制。

默认情况下,工作程序守护进程的堆空间最大为 512MB。可以通过调整工作程序的 fork 选项来更改此值。

可执行文件

只有当守护进程使用相同的 Java 可执行文件时,才会将其视为兼容。

类路径

只有当守护进程的类路径包含所有请求的类路径条目时,才会将其视为兼容。
请注意,只有当类路径与请求的类路径完全匹配时,才会将守护进程视为兼容。

堆设置

只有当守护进程的堆大小设置至少与请求的设置相同时,才会将其视为兼容。
换句话说,堆设置高于请求的守护进程将被视为兼容。

JVM 参数

只有当守护进程设置了所有请求的 JVM 参数时,才会将其视为兼容。
请注意,如果守护进程具有超出请求的附加 JVM 参数(除了堆设置、断言、调试等特殊处理的参数之外),则该守护进程是兼容的。

系统属性

只有当守护进程设置了所有请求的系统属性且值相同时,才会将其视为兼容。
请注意,如果守护进程具有超出请求的附加系统属性,则该守护进程是兼容的。

环境变量

只有当守护进程设置了所有请求的环境变量且值相同时,才会将其视为兼容。
请注意,如果守护进程具有超出请求的环境变量,则该守护进程是兼容的。

引导类路径

只有当守护进程包含所有请求的引导类路径条目时,才会将其视为兼容。
请注意,如果守护进程具有超出请求的引导类路径条目,则该守护进程是兼容的。

调试

只有当调试设置为与请求相同的值(truefalse)时,才会将守护进程视为兼容。

启用断言

只有当启用断言设置为与请求相同的值(truefalse)时,才会将守护进程视为兼容。

默认字符编码

只有当默认字符编码设置为与请求相同的值时,才会将守护进程视为兼容。

工作程序守护进程将一直运行,直到启动它们的构建守护进程停止或系统内存变得稀缺。当系统内存不足时,Gradle 将停止工作程序守护进程以最大程度地减少内存消耗。

开发并行任务 部分中,可以找到将普通任务操作转换为使用工作程序 API 的分步说明。

取消和超时

为了支持取消(例如,当用户使用 CTRL+C 停止构建时)和任务超时,自定义任务应响应中断其执行线程。通过工作程序 API 提交的工作项也是如此。如果任务在 10 秒内没有对中断做出响应,守护进程将关闭以释放系统资源。