2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Coroutineの基礎

Last updated at Posted at 2022-03-26

image.png

本記事は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]

launchcoroutine 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は,すべての子が完了するまで完了しない
2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?