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 任务,以允许用户配置他们想要使用的 codec 库的版本。它将在运行时解析库的相应版本,并配置 worker 以使用此版本。

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 为 codec 库类路径公开一个输入属性。
2 在创建工作队列时,在 ClassLoaderWorkerSpec 上配置类路径。

接下来,您需要配置您的构建,使其具有一个仓库,以便在任务执行时查找 codec 版本。我们还创建一个依赖项,以从此仓库解析我们的 codec 库

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 添加一个仓库以解析 codec 库 - 这可以是一个与用于构建 CreateMD5 任务类的仓库不同的仓库。
2 添加一个配置以解析我们的 codec 库版本。
3 配置 Apache Commons Codec 的备用兼容版本。
4 配置 md5 任务以使用该配置作为其类路径。请注意,该配置将在任务执行之前不会被解析。

现在,如果您运行您的任务,它应该可以按预期工作,并使用配置的 codec 库版本

$ 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. 创建 Worker 守护进程

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

Worker API 可以使用 processIsolation() 方法来适应这种情况,该方法使工作在单独的“worker 守护进程”中执行。这些 worker 进程将是会话范围的,并且可以在同一构建会话中重用,但它们不会跨构建持久存在。但是,如果系统资源不足,Gradle 将停止未使用的 worker 守护进程。

要利用 worker 守护进程,请在创建 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

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

$ 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 必须为每个 worker 守护进程启动一个新进程,这很耗费资源。

但是,如果您第二次运行任务,您会发现它运行速度快得多。这是因为在初始构建期间启动的 worker 守护进程已持久存在,并且可以在后续构建期间立即使用

$ 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() 添加的任何其他类路径条目。此外,该进程将是一个worker 守护进程,它将保持活动状态,并且可以为具有相同要求的未来工作项重用。可以使用与 Gradle JVM 不同的设置配置此进程,方法是使用 ProcessWorkerSpec.forkOptions(org.gradle.api.Action)

Worker 守护进程

当使用 processIsolation() 时,Gradle 将启动一个长期存在的worker 守护进程进程,该进程可以为未来的工作项重用。

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
    }
}

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

默认情况下,worker 守护进程启动时的最大堆大小为 512MB。可以通过调整 worker 的 fork 选项来更改此设置。

可执行文件

仅当守护进程使用相同的 Java 可执行文件时,才认为它是兼容的。

类路径

如果守护进程的类路径包含所有请求的类路径条目,则认为它是兼容的。
请注意,仅当守护进程的类路径与请求的类路径完全匹配时,才认为它是兼容的。

堆设置

如果守护进程具有至少与请求相同的堆大小设置,则认为它是兼容的。
换句话说,堆设置高于请求的守护进程将被认为是兼容的。

JVM 参数

如果守护进程已设置所有请求的 JVM 参数,则它是兼容的。
请注意,如果守护进程除了请求的 JVM 参数之外还有其他 JVM 参数(除了那些特殊处理的参数,例如堆设置、断言、调试等),则它是兼容的。

系统属性

如果守护进程已设置所有请求的系统属性并具有相同的值,则认为它是兼容的。
请注意,如果守护进程除了请求的系统属性之外还有其他系统属性,则它是兼容的。

环境变量

如果守护进程已设置所有请求的环境变量并具有相同的值,则认为它是兼容的。
请注意,如果守护进程的环境变量多于请求的环境变量,则它是兼容的。

引导类路径

如果守护进程包含所有请求的引导类路径条目,则认为它是兼容的。
请注意,如果守护进程的引导类路径条目多于请求的引导类路径条目,则它是兼容的。

调试

仅当调试设置为与请求相同的值(truefalse)时,才认为守护进程是兼容的。

启用断言

仅当启用断言设置为与请求相同的值(truefalse)时,才认为守护进程是兼容的。

默认字符编码

仅当默认字符编码设置为与请求相同的值时,才认为守护进程是兼容的。

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

有关将普通任务操作转换为使用 worker API 的分步描述,请参见关于开发并行任务的部分。

取消和超时

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