ひさびさのGradleブログ。
こちらの公式ドキュメントの「The Worker API」のパートを参考に手を動かしていく。
[事前準備]
Worker APIを利用して並列処理を行う前に、処理本体の実装と、パラメータを用意する必要がある。
以下のコードは全てbuild.gradle.ktsファイルに実装していく。
パラメータはWorkParameterインタフェースを利用して定義する。
以下の例では、ファイルとディレクトリをパラメータにもつReverseParametersを定義している。
interface ReverseParams: WorkParameters {
val fileToReverse: RegularFileProperty
val destDir: DirectoryProperty
}
処理本体はWorkActionインタフェースを利用して実装する。
abstract class ReverseFile @Inject constructor(
val fileSystemOperations: FileSystemOperations
): WorkAction<ReverseParameters> {
override fun execute() {
println("started ${this} ${System.currentTimeMillis()}")
fileSystemOperations.copy {
from(parameters.fileToReverse)
into(parameters.destDir)
// コピーしたファイルの行を反転させる
filter { line: String -> line.reversed() }
}
// 並列処理の確認のためスリープ
Thread.sleep(2000)
println("finished ${this} ${System.currentTimeMillis()}")
}
}
[タスクでWorker APIを呼び出す]
タスクでWorker APIを呼び出すには、コンストラクタでWorkerExecutorクラスのインスタンスを受け取る必要がある。これはGradleが勝手にDIしてくれるらしい。便利。
abstract class ReverseFiles @Inject constructor(
private val exec: WorkerExecutor
): SourceTask() {
@get: OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun reverseFiles() {
val queue = exec.noIsolation()
source.forEach { file ->
queue.submit(ReverseFile::class) {
fileToReverse.set(file)
destDir.set(outputDir)
}
}
queue.await()
logger.lifecycle("Created ${outputDir.get().asFile.listFiles().size} reversed files")
}
}
こうして実装したタスクを、プロジェクトのタスクとして登録する必要がある。
このあたりはDeveloping Parallel Tasks using the Worker APIのドキュメントを参考に進めた。
val target = file("target")
tasks.register<ReverseFiles>("rev") {
group = "custom"
description = "reverse files"
setSource(file("src"))
outputDir.set(target)
}
これで、srcディレクトリにあるファイルをtargetディレクトリにコピーして中身を反転させる、という処理を並列で実行してくれるはずである。
動作確認のためsrcディレクトリには、ファイル名と中身が同じファイルを用意した。
$ tree
.
├── build.gradle.kts
└── src
├── bar
├── foo
├── foobar
├── foobarbaz
├── fuga
└── hoge
1 directory, 7 files
$ cat src/foobar
foobar
実際にタスクを実行してみる。
$ gradle rev > Task :rev started Build_gradle$ReverseFile$Inject@7ae7ada9 1630223219663 started Build_gradle$ReverseFile$Inject@27a0b491 1630223219664 started Build_gradle$ReverseFile$Inject@4a89a2b3 1630223219665 started Build_gradle$ReverseFile$Inject@743ddbe6 1630223219668 finished Build_gradle$ReverseFile$Inject@7ae7ada9 1630223221668 started Build_gradle$ReverseFile$Inject@a2bb9e1 1630223221669 finished Build_gradle$ReverseFile$Inject@27a0b491 1630223221669 finished Build_gradle$ReverseFile$Inject@4a89a2b3 1630223221669 started Build_gradle$ReverseFile$Inject@57b13a9c 1630223221670 finished Build_gradle$ReverseFile$Inject@743ddbe6 1630223221673 finished Build_gradle$ReverseFile$Inject@57b13a9c 1630223223676 finished Build_gradle$ReverseFile$Inject@a2bb9e1 1630223223676 Created 6 reversed files BUILD SUCCESSFUL in 4s 1 actionable task: 1 executed
処理結果を確認すると期待通り反転した文字列が含まれたファイルが出力されていた。
$ tree . ├── build.gradle.kts ├── src │ ├── bar │ ├── foo │ ├── foobar │ ├── foobarbaz │ ├── fuga │ └── hoge └── target ├── bar ├── foo ├── foobar ├── foobarbaz ├── fuga └── hoge 2 directories, 13 files $ cat target/foobar raboof
2秒待つ処理が6本並列で実行されるから、2秒で終わるかと思ったがそうではないようだ。最初にstartedのログが4つ出力され、1つ終了するとすぐに5つ目の処理が開始されていることから、デフォルトでは4並列で処理が実行されるらしい。
ためしに--max-workersオプションを指定して以下のように実行してみると、2秒で処理が終わった。
$ gradle rev --max-workers=6 > Task :rev started Build_gradle$ReverseFile$Inject@96412a3 1630223429496 started Build_gradle$ReverseFile$Inject@621c6f54 1630223429496 started Build_gradle$ReverseFile$Inject@7980e25e 1630223429497 started Build_gradle$ReverseFile$Inject@a3fb4d9 1630223429498 started Build_gradle$ReverseFile$Inject@10858a01 1630223429499 started Build_gradle$ReverseFile$Inject@7522d7fa 1630223429500 finished Build_gradle$ReverseFile$Inject@7980e25e 1630223431501 finished Build_gradle$ReverseFile$Inject@10858a01 1630223431502 finished Build_gradle$ReverseFile$Inject@96412a3 1630223431501 finished Build_gradle$ReverseFile$Inject@a3fb4d9 1630223431501 finished Build_gradle$ReverseFile$Inject@621c6f54 1630223431501 finished Build_gradle$ReverseFile$Inject@7522d7fa 1630223431502 Created 6 reversed files BUILD SUCCESSFUL in 2s 1 actionable task: 1 executed
Worker APIでの並列処理はWorkQueueを取得する際にJVMの分離度を指定できる。
- WorkerExecutor.noIsolation():最速。クラスローダも同じ。
- WorkerExecutor.classLoaderIsolation():クラスローダを別にできる。
- WorkerExecutor.processIsolation():クラスローダだけでなく、メモリの設定などもGradle本体と別にできる。
[Worker APIを使わない場合]
Worker APIを使わない場合、別のサブプロジェクトに属するタスクであれば、org.gradle.parallel=trueをgradle.propertiesで指定する、または--parallelオプションを指定して実行することで、並列実行できる。
以下のようなbuild.gradle.ktsファイルを用意する。
plugins {
base
}
repeat(5) { counter ->
project("p$counter") {
tasks.register("task$counter") {
doLast {
Thread.sleep(1000L * counter)
println("I'm task number $counter")
}
}
}
}
tasks.build {
dependsOn(":p0:task0")
dependsOn(":p1:task1")
dependsOn(":p2:task2")
dependsOn(":p3:task3")
dependsOn(":p4:task4")
}
プロジェクトの設定のため、以下のsettings.gradle.ktsファイルを用意する。
include("p0", "p1", "p2", "p3", "p4")
普通に実行してみると、以下のように10秒ほどかかる。
$ gradle build > Task :p0:task0 I'm task number 0 > Task :p1:task1 I'm task number 1 > Task :p2:task2 I'm task number 2 > Task :p3:task3 I'm task number 3 > Task :p4:task4 I'm task number 4 BUILD SUCCESSFUL in 10s 5 actionable tasks: 5 executed
しかし、--parallelオプションを指定して実行すると、以下のように4秒程度で完了する。
$ gradle build --parallel --max-workers=6 > Task :p0:task0 I'm task number 0 > Task :p1:task1 I'm task number 1 > Task :p2:task2 I'm task number 2 > Task :p3:task3 I'm task number 3 > Task :p4:task4 I'm task number 4 BUILD SUCCESSFUL in 4s 5 actionable tasks: 5 executed