Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

完全に理解した気になるKotlin Coroutines

More than 1 year has passed since last update.

coroutinesを使うとスレッドをブロックしたり、コールバックなしにコードを書いていくことができます。
ただ、自分がコードを見たときには魔法にしか見えなかったので、coroutinesの本質に迫っていきたいと考えました。

launch coroutineビルダーによる実行

まず最も利用されるlaunchについて説明していきます。
以下のように書いた時にThreadはどうなっているでしょうか?

launch {
  println("thread:"+Thread.currentThread())
}

以下のようにログが出力されます。
thread:Thread[ForkJoinPool.commonPool-worker-2,5,main]

このForkJoinPool.commonPool-workerはどこから来ているのでしょうか?
launchの引数のDefaultDispatcherを見てきましょう。実はこの引数のCoroutineContextによって実行されるThreadが決まります。

public fun launch(
    context: CoroutineContext = DefaultDispatcher, 
    start: CoroutineStart = CoroutineStart.DEFAULT,
    parent: Job? = null,
    onCompletion: CompletionHandler? = null,
    block: suspend CoroutineScope.() -> Unit
): Job {

DefaultDispatcherは以下のようになっています。つまりDefaultDispatcherはCommonPoolです。

public actual val DefaultDispatcher: CoroutineDispatcher = CommonPool

CommonPoolはどうなっているでしょうか?
https://github.com/Kotlin/kotlinx.coroutines/blob/1ce6c0b223db39ce8a75ae08b9d9624a6554e34f/core/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CommonPool.kt

Thread Poolがなければ作り渡されたblockをThread Poolでexecuteしています。ここで実際にprintln("thread:"+Thread.currentThread())が実行されます。

    override fun dispatch(context: CoroutineContext, block: Runnable) =
        try { (pool ?: getOrCreatePoolSync()).execute(timeSource.trackTask(block)) }
        catch (e: RejectedExecutionException) {
            timeSource.unTrackTask()
            DefaultExecutor.execute(block)
        }

実際に自分でDispatcherを作るとどうなるでしょうか?
以下のようにそのままのThreadで実行するようなことができます。

fun main(args: Array<String>) {
    launch(MyDispacher()) {
        println("thread:"+Thread.currentThread())
    }
}

class MyDispacher : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        println("dispatch:before")
        block.run()
        println("dispatch:after")
    }
}

Thread Poolで実行していないので、Main Threadでそのまま実行されていることが分かります。

dispatch:before
thread:Thread[main,5,main]
dispatch:after

ここまではCoroutineってthreadと同じようなものだと思うかもしれません。
Coroutineは中断ができます

中断

suspendCoroutine()はdelay()やawait()などといった中断を行うメソッドの中で使われるメソッドです。
これを使うとCoroutineの中断をすることができます。
中断すると何が起こるかというと、そのThreadでの実行を一度やめて、もう一度走り直します。
resumeを使うことで、中断されたCoroutinesを再開することができます。

fun main(args: Array<String>) {
    launch(MyDispatcher()) {
        println("1:" + Thread.currentThread())
        suspendCoroutine<String> { continuation ->
            thread {
                // 普通にthreadを作って実行する
                // 中断されたCoroutinesを再開する
                continuation.resume("test")
            }
        }
        println("2:" + Thread.currentThread())
    }

    Thread.sleep(1000)
}

class MyDispatcher : CoroutineDispatcher() {
    val executor = Executors.newSingleThreadExecutor()
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executor.execute {
            println("dispatch:before")
            block.run()
            println("dispatch:after")
        }
    }
}
dispatch:before
1:Thread[pool-1-thread-1,5,main]
dispatch:after
# **ここで一度中断している**
dispatch:before
2:Thread[pool-1-thread-1,5,main]
dispatch:after

実践Coroutines

Androidではlaunch(UI)を利用するとUI Threadで実行します。想像がつくかもしれませんが、launch(UI)を利用するとAndroidのHandlerによって実行されます

launch(UI) {
  val response = api.fetch()
  Snackbar.make(view, "code:${response.code}", Snackbar.LENGTH_LONG)
    .setAction("Action", null).show()
}

HandlerContextの中で、AndroidのHandlerにただRunnableをpostしているだけで簡単ですね。

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }

今回のfetch()メソッドの中は以下のようになっています。

    interface ApiSearvice {
        @GET("/")
        fun fetchDefferd(@Query("zipcode") zipCode: String = "1000000"): Deferred<Response>
    }

    val service = retrofit.create(ApiSearvice::class.java)

    // わざわざsuspendメソッドでラップしている
    suspend fun fetch(): Response {
        return service.fetchDefferd().await()
    }

retrofit2-kotlin-coroutines-adapterを使っていますが、わざわざsuspendメソッドでラップしています。なぜなら使う側で中断関数await()を呼ぶ必要がなくなるからです。
こちらについてはKotlinのドキュメントのAsynchronous programming stylesを読むで説明していて、suspendを使ってawaitをなるべく使わないようにすることで、よりKotlinらしく書くことができると記載があります。(retrofit2-kotlin-coroutines-adapterはissueも上がっていてこれに対応してほしい。直せるかなってcloneして見てみたけどRetrofitの仕組み的に難しそうだった。。)
これまでの傾向で、どのThreadで通信処理が動いているのか気になるかもしれません。通信処理はOkHttpのスレッドで実行されて、await()のタイミングでCorouitnesが中断して、取得でき次第再開します。

終わりに

どうでしょうか、完全に理解できましたかね?多分無理だと思うので、
ここまで読んだ上で@k-kagurazaka@githubさんの入門Kotlin coroutinesに入るとすんなり理解できてくるかもしれません。

takahirom
Google Developers Expert for Android
cyberagent
サイバーエージェントは「21世紀を代表する会社を創る」をビジョンに掲げ、インターネットテレビ局「AbemaTV」の運営や国内トップシェアを誇るインターネット広告事業を展開しています。インターネット産業の変化に合わせ新規事業を生み出しながら事業拡大を続けています。
http://www.cyberagent.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away