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` 任务,允许用户配置他们想要使用的编解码器库版本。它将在运行时解析适当的库版本,并配置 workers 以使用此版本。

`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 的最大堆启动。这可以通过调整 worker 的 fork 选项来更改。

可执行文件

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

类路径

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

堆设置

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

JVM 参数

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

系统属性

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

环境变量

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

引导类路径

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

调试

只有当调试设置为与请求的值相同(`true` 或 `false`)时,守护进程才被认为是兼容的。

启用断言

只有当启用断言设置为与请求的值相同(`true` 或 `false`)时,守护进程才被认为是兼容的。

默认字符编码

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

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

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

取消和超时

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