本記事はKotlin公式documentで学習を進める上で読んでまとめたnoteです。
詳細は公式から参照してください↓
Coroutine
Kotlinは,言語として,他のさまざまな libraryがcoroutine
を利用できるようにするために,標準 libraryで最小限の低 levelAPIのみを提供します。Kotlinではasyncとawaitは keywordに含まれず,標準 libraryの一部ではありません。さらに,Kotlinのsuspend
機能の概念はfuturesやpromiseよりも安全で, errorが発生しにくい非同期処理の抽象化を提供します。
【Coroutines basics】
coroutineを理解する上で、suspend(中断)
の概念について知っておく必要があります。coroutineでは、threadをblock(占有)する代わりに処理をsuspend(中断)します。 blockとsuspendの違いを見ていきましょう。
-
block
blockは、threadを占有します。占有中は、threadで処理を進めることはできません。fun main(){ println("Coroutine") Thread.sleep(3000L) // 3秒間、処理が停止。この間、処理は一切進まない。 println("Hello") }
Coroutine Hello
-
suspend
suspendは、coroutineの処理をを中断し、threadを解放します。解放中は、他の処理にresourceを活用できます。fun main() = runBlocking { launch{ delay(3000L) // 3秒間、処理を中断。threadが解放されるので先にHelloが表示される println("Coroutine") } println("Hello") }
Hello Coroutine
前者の書き方では3秒の間、threadがblockされてしまうという問題があります。たとえば、applicationの使用中にthreadを占有してしまうとfreezeしてuserが画面を操作することができなくなります。 一方、後者の場合はsuspendするだけなので3秒の間、userは操作が可能です。
coroutineのsuspend(中断)
について理解を深めたところで実際に使っていきます。
[Your first coroutine]
coroutine
はsuspendable computationの instanceです。他の codeと同時に動作する code blockを実行するという意味で, thread
と概念的に似ています。しかし, coroutineは特定の threadに bindされません。ある threadで実行をsuspendし,別の threadで再開することができます。
coroutineは軽量の threadと考えることができますが,実際の使用方法を threadとは大きく違う点がいくつかあります。
fun main() = runBlocking { // this: coroutineScope
launch { // 新しいcoroutineを起動
delay(1000L) // non-blockingで1秒遅延
println("World!") // 1秒後に表示
}
println("Hello") // 前のthreadが遅れている間に,main threadが実行される
}
Hello
World!
続いて,この codeが何をするかを分析してみましょう。
-
launch
はcoroutine builderです。残りの codeと同時に新しいcoroutineを起動し,独立して機能し続けます。そのため,Helloが最初に表示されました。 -
delay
は特殊なsuspend関数です。coroutineを特定の時間だけ処理を中断し、基となるthreadを解放します。delayを記述するthreadはblockされますが基となるthreadはblockされません(non-blocking)。 -
runBlocking
は,fun main()
の非coroutineの世界とrunBlocking{...}
の block内のcoroutineを含む codeの橋渡しをするcoroutineでもあります。たとえば,launchはcoroutineScopeのみで 宣言されます。
runBlockingの名前は,runBlocking {...}内のすべてのcoroutineが実行を完了するまで,それを実行する thread(この場合は main thread)が call中に blockされることを意味します。
高価な resourceである threadを blockすることは非効率的であり,望ましくないことが多いため実際の code内で
runBlocking
が使用されることはほとんどありません。
[Structured concurrency]
coroutineはStructured concurrencyの原則
に従います。つまり,新しいcoroutineは,coroutineの存続期間を区切る特定のcoroutineScopeでのみ起動できます。実際の applicationでは,多くのcoroutineが起動しますが,Structured concurrencyにより,それらが失われたり,leakしたりすることがなくなります。
[Extract function refactoring]
launch{...}
内の code blockを別の関数に抽出してみましょう。coroutineScope内の codeを抽出する場合,suspend
修飾子を使用します。
fun main() = runBlocking { // this: coroutineScope
launch { doWorld() }
println("Hello")
}
suspend fun doWorld() {
delay(1000L)
println("World!")
}
[Scope builder]
さまざまな builderによって提供されるcoroutine scopeに加えて,coroutineScope
builderを使用して独自の scopeを 宣言することができます。coroutine scopeを作成し,起動されたすべての子が完了するまで完了しません。
runBlocking
builderとcoroutineScope
builderは,どちらも自分の体とそのすべての子が完了するのを待つため,一見似ているよう見えます。主な違いは次のようになります。
-
runBlocking
: currentの threadを blockして待機します。 -
coroutineScope
: はsuspendし,基になる threadを他の用途に解放します。
この違いのため,runBlockingは通常の関数, coroutineScopeはsuspend関数として実行されます。
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: coroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
sample code
fun main() = runBlocking { // serial
countToDown()
println("fire!")
}
suspend fun countToDown() = coroutineScope { // concurrent
launch {
delay(2000L)
println("1")
}
launch {
delay(1000L)
println("2")
}
println("3")
}
3
2
1
fire!
[An explicit job]
launch
coroutine builderは,起動されたcoroutineへの handleであり,その完了を明示的に待機するために使用できるJob
objectを返します。たとえば,子coroutineの完了を待ってから,「Done」文字列を出力できます。
val job = launch { // 新しくcoroutineを起動し,そのjobへの参照を保持します
delay(1000L)
println("World!")
}
println("Hello")
job.join() // 子coroutineが完了するまで待ちます
println("Done")
Hello
World!
Done
[Coroutines are light-weight]
100Kのcoroutineを起動し,5秒後に各 coroutineが.
を出力します。軽量なcoroutineでの実行は成功しますが,Threadで置き換えた場合,memory leakを引き起こします。
fun main() {
launchCoroutine() // success!
runThread() // java.lang.OutOfMemoryError
}
private fun launchCoroutines() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
private fun runThreads() {
repeat(100_000) { // launch a lot of threads
object:Thread() {
override fun run() {
sleep(5000L)
print(".")
}
}.start()
}
}
まとめ
今回,学んだ内容で重要だと思う点をまとめてみました。
- 軽量で安全に扱える非同期処理の1つ
- threadはblockせずにsuspend(中断)を使用する
- 特定のthreadにbindされないので,別のthreadで再開できる
- coroutineScope内で起動されたcoroutineは,すべての子が完了するまで完了しない