GradleのWorker APIでタスクを並列実行する

ひさびさのGradleブログ。

こちらの公式ドキュメントの「The Worker API」のパートを参考に手を動かしていく。

docs.gradle.org

 

 

[事前準備]

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=truegradle.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

 

[まとめ]

  • Worker APIを利用した並列処理を実装してみた
  • Worker APIを利用するとGradle本体とJVMの分離度を指定できる
  • Worker APIを使わなくてもサブプロジェクトのタスクは並列実行できる
  • サンプルコードは以下のリポジトリにまとめてある

github.com