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

写作任务5

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

工人API

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

工作线程 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/爱因斯坦.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扩展名包含该目录中文件的 MD5 哈希值src。请注意,该任务至少需要 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服务才能提交您的工作。创建一个带注释的抽象 getter 方法javax.inject.Inject,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()模式是最低级别的隔离,将防止工作单元更改项目状态。这是最快的隔离模式,因为它需要最少的开销来设置和执行工作项。但是,它将使用单个共享类加载器来完成所有工作单元。这意味着每个工作单元都可以通过静态类状态相互影响。这也意味着每个工作单元在 buildscript 类路径上使用相同版本的库。如果您希望用户能够将任务配置为使用不同(但兼容)版本的 Apache Commons Codec库运行,则需要使用不同的隔离模式。

首先,您必须将依赖项更改为buildSrc/build.gradlebe 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()使工作在单独的“worker 守护进程”中执行的方法来适应这种情况。这些工作守护进程将在构建过程中持续存在,并且可以在后续构建过程中重用。但是,如果系统资源不足,Gradle 将停止未使用的工作守护进程。

要使用工作守护进程,请processIsolation()在创建WorkQueue.您可能还想为新流程配置自定义设置:

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 的最大堆启动。这可以通过调整工人的分叉选项来改变。

可执行文件

仅当守护程序使用相同的 Java 可执行文件时,该守护程序才被视为兼容。

类路径

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

堆设置

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

jvm 参数

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

系统属性

如果守护程序已将请求的所有系统属性设置为相同的值,则该守护程序被视为兼容。
请注意,如果守护程序具有超出所要求的其他系统属性,则该守护程序是兼容的。

环境变量

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

引导类路径

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

调试

仅当 debug 设置为与请求的值相同(truefalse)时,守护程序才被视为兼容。

启用断言

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

默认字符编码

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

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

有关将普通任务操作转换为使用辅助 API 的分步说明可以在开发并行任务部分中找到。

取消和超时

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