LoginSignup
100
92

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-01

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に入るとすんなり理解できてくるかもしれません。

100
92
0

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
100
92