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 hash 檔案可能會以不同的順序產生,因為工作單元會並行執行。然而,這次工作任務執行得快得多。這是因為 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` 工作任務,以允許使用者設定他們想要使用的編解碼器函式庫版本。它會在執行時期解析函式庫的適當版本,並設定工作人員使用此版本。

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 版本不相容,且可能需要使用不同版本執行。

工作器 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 的最大堆積啟動。這可透過調整工作程序的分岔選項來變更。

可執行檔

只有使用相同 Java 可執行檔的守護程式才視為相容。

類別路徑

只有類別路徑包含所有要求的類別路徑項目時,才視為相容的守護程式。
請注意,只有類別路徑與要求的類別路徑完全相符時,才視為相容的守護程式。

堆積設定

只有堆積大小設定至少與要求的相同時,才視為相容的守護程式。
換句話說,堆積設定高於要求的守護程式會視為相容。

JVM 引數

只有設定所有要求的 JVM 引數時,才視為相容的守護程式。
請注意,如果守護程式有額外的 JVM 引數(除了特別處理的,例如堆積設定、斷言、偵錯等),則視為相容。

系統屬性

只有設定所有要求的系統屬性且具有相同值時,才視為相容的守護程式。
請注意,如果守護程式有額外的系統屬性(除了要求的),則視為相容。

環境變數

只有設定所有要求的環境變數且具有相同值時,才視為相容的守護程式。
請注意,如果守護程式有比要求的更多環境變數,則視為相容。

開機類別路徑

只有包含所有要求的開機類別路徑項目時,才視為相容的守護程式。
請注意,如果守護程式有比要求的更多開機類別路徑項目,則視為相容。

偵錯

只有將偵錯設定為與要求相同的值(truefalse)時,才視為相容的守護程式。

啟用斷言

只有將啟用斷言設定為與要求相同的值(truefalse)時,才視為相容的守護程式。

預設字元編碼

只有將預設字元編碼設定為與要求相同的值時,才視為相容的守護程式。

工作人員守護程式會持續執行,直到啟動它們的建置守護程式停止或系統記憶體不足。當系統記憶體不足時,Gradle 會停止工作人員守護程式以將記憶體消耗降至最低。

有關將一般任務動作轉換為使用工作人員 API 的逐步說明,請參閱開發平行任務部分。

取消和逾時

若要支援取消(例如,當使用者以 CTRL+C 停止建置時)和工作逾時,自訂工作應對中斷其執行緒做出反應。透過工作人員 API 提交的工作項目也是如此。如果工作在 10 秒內未回應中斷,守護程式會關閉以釋放系統資源。