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