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

这使得 Gradle 能够充分利用可用资源,并更快地完成构建。
Worker API
Worker API 提供了将任务操作的执行分解为离散的工作单元,然后并发和异步执行这些工作的能力。
Worker API 示例
理解如何使用 API 的最佳方法是完成将现有自定义任务转换为使用 Worker API 的过程
-
您将首先创建一个自定义任务类,该类为可配置的文件集生成 MD5 哈希值。
-
然后,您将转换此自定义任务以使用 Worker API。
-
然后,我们将探索以不同的隔离级别运行任务。
在此过程中,您将了解 Worker API 的基础知识及其提供的功能。
步骤 1. 创建自定义任务类
首先,创建一个自定义任务,该任务为可配置的文件集生成 MD5 哈希值。
在一个新目录中,创建一个 buildSrc/build.gradle(.kts)
文件
repositories {
mavenCentral()
}
dependencies {
implementation("commons-io:commons-io:2.5")
implementation("commons-codec:commons-codec:1.9") (1)
}
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
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
任务
plugins { id("base") } (1)
tasks.register<CreateMD5>("md5") {
destinationDirectory = project.layout.buildDirectory.dir("md5") (2)
source(project.layout.projectDirectory.file("src")) (3)
}
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
目录中创建三个文件
Intellectual growth should commence at birth and cease only at death.
I was born not knowing and have had only a little time to change that here and there.
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 哈希文件的生成,工作单元将需要两个参数
-
要哈希的文件,以及
-
要将哈希值写入的文件。
无需创建具体实现,因为 Gradle 将在运行时为我们生成一个。
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
的抽象类
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,而不是自己执行工作。
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
上有三种方法可以控制这一点
-
noIsolation()
-
classLoaderIsolation()
-
processIsolation()
noIsolation()
模式是最低级别的隔离,将阻止工作单元更改项目状态。这是最快的隔离模式,因为它设置和执行工作项所需的开销最少。但是,它将为所有工作单元使用单个共享类加载器。这意味着每个工作单元都可能通过静态类状态相互影响。这也意味着每个工作单元都使用构建脚本类路径上相同版本的库。如果您希望用户能够配置任务以使用不同(但兼容)版本的 Apache Commons Codec 库运行,则需要使用不同的隔离模式。
首先,您必须将 buildSrc/build.gradle
中的依赖项更改为 compileOnly
。这告诉 Gradle 在构建类时应使用此依赖项,但不应将其放在构建脚本类路径上
repositories {
mavenCentral()
}
dependencies {
implementation("commons-io:commons-io:2.5")
compileOnly("commons-codec:commons-codec:1.9")
}
repositories {
mavenCentral()
}
dependencies {
implementation 'commons-io:commons-io:2.5'
compileOnly 'commons-codec:commons-codec:1.9'
}
接下来,更改 CreateMD5
任务,以允许用户配置他们想要使用的 codec 库的版本。它将在运行时解析库的相应版本,并配置 worker 以使用此版本。
classLoaderIsolation()
方法告诉 Gradle 在具有隔离类加载器的线程中运行此工作
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 库
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"))
}
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()
方法。您可能还想为新进程配置自定义设置
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 守护进程进程,该进程可以为未来的工作项重用。
// 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
}
}
// 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 参数(除了那些特殊处理的参数,例如堆设置、断言、调试等),则它是兼容的。 - 系统属性
-
如果守护进程已设置所有请求的系统属性并具有相同的值,则认为它是兼容的。
请注意,如果守护进程除了请求的系统属性之外还有其他系统属性,则它是兼容的。 - 环境变量
-
如果守护进程已设置所有请求的环境变量并具有相同的值,则认为它是兼容的。
请注意,如果守护进程的环境变量多于请求的环境变量,则它是兼容的。 - 引导类路径
-
如果守护进程包含所有请求的引导类路径条目,则认为它是兼容的。
请注意,如果守护进程的引导类路径条目多于请求的引导类路径条目,则它是兼容的。 - 调试
-
仅当调试设置为与请求相同的值(
true
或false
)时,才认为守护进程是兼容的。 - 启用断言
-
仅当启用断言设置为与请求相同的值(
true
或false
)时,才认为守护进程是兼容的。 - 默认字符编码
-
仅当默认字符编码设置为与请求相同的值时,才认为守护进程是兼容的。
Worker 守护进程将保持运行,直到启动它们的构建守护进程停止或系统内存变得稀缺。当系统内存不足时,Gradle 将停止 worker 守护进程以最大限度地减少内存消耗。
有关将普通任务操作转换为使用 worker API 的分步描述,请参见关于开发并行任务的部分。 |
取消和超时
为了支持取消(例如,当用户使用 CTRL+C 停止构建时)和任务超时,自定义任务应该对中断其执行线程做出反应。通过 worker API 提交的工作项也是如此。如果任务在 10 秒内未响应中断,则守护进程将关闭以释放系统资源。