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 服務才能提交您的工作。建立一個抽象 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() 模式是最低級別的隔離,它將阻止工作單元變更專案狀態。這是最快的隔離模式,因為它設定和執行工作項目所需的開銷最少。但是,它將為所有工作單元使用單個共享類別載入器。這表示每個工作單元都可以透過靜態類別狀態相互影響。這也表示每個工作單元都使用建置腳本類別路徑上相同版本的函式庫。如果您希望使用者能夠配置任務以使用不同(但相容)版本的 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 Daemon

有時,在執行工作項目時,需要使用更高級別的隔離。例如,外部函式庫可能依賴於設定某些系統屬性,這些屬性在工作項目之間可能會發生衝突。或者函式庫可能與 Gradle 正在運行的 JDK 版本不相容,可能需要使用不同的版本運行。

Worker API 可以使用 processIsolation() 方法來滿足此需求,該方法會導致工作在單獨的「worker daemon」中執行。這些 worker 程序將是會話範圍的,並且可以在同一個建置會話中重複使用,但它們不會跨建置持續存在。但是,如果系統資源不足,Gradle 將停止未使用的 worker daemon。

要使用 worker daemon,請在建立 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 daemon 而不是其他方式

$ 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 daemon 啟動一個新程序,這很耗資源。

但是,如果您第二次執行您的任務,您會發現它的運行速度快得多。這是因為在初始建置期間啟動的 worker daemon 已持續存在,並且可以在後續建置期間立即使用

$ 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 daemon,它將保持活動狀態,並且可以重複用於具有相同需求的未來工作項目。可以使用 ProcessWorkerSpec.forkOptions(org.gradle.api.Action),使用與 Gradle JVM 不同的設定來配置此程序。

Worker Daemons

當使用 processIsolation() 時,Gradle 將啟動一個長期存在的worker daemon 程序,該程序可以重複用於未來的工作項目。

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 daemon 的工作單元時,Gradle 將首先查看是否已存在相容的閒置 daemon。如果是,它將把工作單元發送到閒置的 daemon,並將其標記為忙碌。如果沒有,它將啟動一個新的 daemon。在評估相容性時,Gradle 會查看許多標準,所有這些標準都可以透過 ProcessWorkerSpec.forkOptions(org.gradle.api.Action) 來控制。

預設情況下,worker daemon 以 512MB 的最大堆積啟動。可以透過調整 worker 的 fork 選項來變更此設定。

executable

僅當 daemon 使用相同的 Java 可執行檔時,才認為它是相容的。

classpath

如果 daemon 的類別路徑包含所有請求的類別路徑條目,則認為它是相容的。
請注意,僅當 daemon 的類別路徑與請求的類別路徑完全匹配時,才認為它是相容的。

heap settings

如果 daemon 具有至少與請求的堆積大小設定相同的堆積大小設定,則認為它是相容的。
換句話說,具有比請求的堆積設定更高的 daemon 將被認為是相容的。

jvm arguments

如果 daemon 已設定所有請求的 JVM 參數,則它是相容的。
請注意,如果 daemon 除了請求的 JVM 參數之外還有其他 JVM 參數(除了那些經過特殊處理的參數,例如堆積設定、斷言、偵錯等),則它是相容的。

system properties

如果 daemon 已設定所有請求的系統屬性並具有相同的值,則認為它是相容的。
請注意,如果 daemon 除了請求的系統屬性之外還有其他系統屬性,則它是相容的。

environment variables

如果 daemon 已設定所有請求的環境變數並具有相同的值,則認為它是相容的。
請注意,如果 daemon 具有比請求的環境變數更多的環境變數,則它是相容的。

bootstrap classpath

如果 daemon 包含所有請求的 bootstrap 類別路徑條目,則認為它是相容的。
請注意,如果 daemon 具有比請求的 bootstrap 類別路徑條目更多的 bootstrap 類別路徑條目,則它是相容的。

debug

僅當偵錯設定為與請求的值相同(truefalse)時,才認為 daemon 是相容的。

enable assertions

僅當啟用斷言設定為與請求的值相同(truefalse)時,才認為 daemon 是相容的。

default character encoding

僅當預設字元編碼設定為與請求的值相同時,才認為 daemon 是相容的。

Worker daemon 將保持運行,直到啟動它們的建置 daemon 停止或系統記憶體變得稀缺。當系統記憶體不足時,Gradle 將停止 worker daemon 以最大限度地減少記憶體消耗。

將正常任務動作轉換為使用 worker API 的逐步說明可以在關於開發平行任務的章節中找到。

取消與逾時

為了支援取消(例如,當使用者使用 CTRL+C 停止建置時)和任務逾時,自訂任務應對中斷其執行線程做出反應。透過 worker API 提交的工作項目也是如此。如果任務在 10 秒內沒有回應中斷,daemon 將關閉以釋放系統資源。