Androidマンになりたいおじさん

自分の備忘録的なのです。Androidを普及できたらいいなと思ったりしながら書きます

Kotlin Coroutineを使いこなしたい(願望)

はじめに

Merry Christmas!!!!!!!!!!!


この記事は学内サークルの記事24日目向けとなっています。

今回はKotlin 1.3リリースより、stableで導入されたCorutineを自分なりに理解して使いこなしたいぞおおおお!って趣旨のやつです!!!!

stable前の記事はたくさん溢れていますが、残念ながら破壊的な変更があったので自分で学びながらまとめてみようかなと思った次第です

let's go

Coroutine????

wiki参照
コルーチン - Wikipedia

うん、わからん!!!!!!!

とりあえずwiki読んでみたここまでの自分なりのCoroutineとは

  • サブルーチンと違って処理を中断できて、中断したとこから続行できる
  • 協調を意識した動作が命名由来(co-routine)
  • 状態管理を意識せずに行える
  • コルーチンはサブルーチンを一般化したもの
  • マルチスレッドで原理的に同じ動作はできるが、同期制御は自分でやれよと

まぁ、自分理解力過疎なのでわからんよねー

先人の考え方を参考にしてみるぜ!!!
@sys1yagiさんのスライド sys1yagi.hatenablog.com

めっちゃ詳しく書かれてて、もうこの人の記事やスライド見ればよいのではないか??????

それでも他の方のも見るべきだな、うんうんって感じで
@k-kagurazakaさんの記事 qiita.com

つまりは、あっちこっちいける軽量スレッド(?)

どうでもいいけど、コルーチンってカタカナださry

参考にさせて頂いたお二人ありがとうございます!!!!!!(土下座

触って慣れる

主にここのdocsを触りつつ、ようわからんとこは他の人の解説見たりして自分で考えて見るという感じで github.com

Kotlin version 1.3.11を使用

Hello, Coroutine

version1.3だから使えるぜやっふーーーではないですね

しっかりとmavenなりgradleなり下記の公式通りに明記してあげましょう github.com

今回僕は最近良く耳にするgradleをkotlin dslで書けるgradle.ktsで書きます

dependencies {
    compile(kotlin("stdlib"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0")
}
fun main() {
    GlobalScope.launch {
        delay(1000L) 
        log("World!") 
    }
    log("Hello,") 
    Thread.sleep(2000L) 
}

どのスレッドが動いてるか分かりやすいようにlog関数定義してます

fun coroutineLog(message: String) {
    println("Thread name {${Thread.currentThread().name}}: $message")
}

上記の結果はこんな感じ

Thread name {main}: Hello,
Thread name {DefaultDispatcher-worker-1}: World!

GlobalScopeはsingletonでimmutableなCoroutineContextプロパティを持ったinterface => CoroutineScopeをimplementsしてるみたい 実装ではカスタムバッキングフィールドでgetされたらEmptyCoroutineContextを返すようになっているっぽい

launchはcoroutineの発射(よし、行ってこい)的な感じで生成されて、CoroutineScopeの拡張関数で定義されている

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

引数にcontext, start, blockをもらって、Jobを返してくれるcoroutine builderさん

GlobalScopeでcoroutine生成すると、トップレベル扱いで生成されるっぽい(まあ、Globalって書いてるしね)。そしてこいつはアプリケーション全体でライフサイクルによって制御されるから、アプリが終わったら処理してる途中とか関係なく死んでしまう...だから途中でdelayとか挟んで制御してるわけってことですな(delayがなかったら、Helloだけで終わる!)

公式にGlobal coroutines are like daemon threadsって書いてるから、GlobalScopeの場合はデーモンスレッドみたいなもんかーでいいと思う

デフォルト引数で実行block以外は設定されてるから、上のコードが成り立ってる訳ですね

そもそも、delay()はノンブロッキングでThread.sleep()はブロッキングでごっちゃになってしまう...
なので、どこがブロッキング処理でどこがノンブロッキング処理なのかを明確にしてあげるためにrunBlocking builderが登場

この方は、さっきまでdelayとかで待ったり制御しないとだめだったのをrunBlocking 内部のコルーチンが完了するまで ブロックしてくれるみたい
じゃあ、delay書かなくてもいいのかーってやってもだめです...GlobalScopeで新たなscopeを書いているから...。

じゃあ、GlobalScope.launchで終わるまで待っててくれーってできるの?
GlobalScope.launchの返り値はJobでした

Jobのjoin()メソッドで解決できるっぽい。この人はlaunch打ち上げられたcoroutineの終わりを健気に待ってくれる

またまた修正

fun main() = runBlocking {
    val job = GlobalScope.launch {
        coroutineLog("World!")
    }
    coroutineLog("Hello,")
    job.join()
}

結果

Thread name {main}: Hello,
Thread name {DefaultDispatcher-worker-1}: World!

よさそう!

でも、threadみたいに扱うなら内部でトップレベルcoroutine生成するのはよくないのでは?と書いてます

なんで?

There is still something to be desired for practical usage of coroutines.
When we use GlobalScope.launch we create a top-level coroutine.
Even though it is light-weight, it still consumes some memory resources while it runs.
If we forget to keep a reference to the newly launched coroutine it still runs.
What if the code in the coroutine hangs (for example, we erroneously delay for too long),
what if we launched too many coroutines and ran out of memory?
Having to manually keep a reference to all the launched coroutines and join them is error-prone.

いくらcoroutineが軽量だからってメモリは使うんだよ。コルーチンの参照保持するの忘れた?関係ないよ。実行させ続けるからな。いつかメモリ足りなくなっても知らないし、それでjoinとかしてエラーなっても知らないよ的な事を言ってます。無責任ですね

だからといって、鬼ではないです。それを解決する仕組みでstructured concurrencyってのを使えばいいよって

runBlocking内部でthis参照で同じscope使ってlaunchしてあげたら同じscope内でちゃんと終わるの待ってくれます

なので、上記を修正

fun main() = runBlocking {
    launch {
        coroutineLog("World!")
    }
    coroutineLog("Hello,")
}

結果

Thread name {main}: Hello,
Thread name {main}: World!

やったぜ!!!
でもまだよくわかってない(((((((((((((

こうすることで明示的にjoinすることがないから、大丈夫だよってことなのかな???

以下、気になったとこ

CoroutineContext

これはandroidと同じ感じでいくと、どこから起動されて、どういう状態か等々のcoroutine生成に必要な情報を持っているって感覚でよいのかな(?)

第二引数start形式について

これに関してはCoroutineStart enum classで以下がある

  • DEFAULT
  • LAZY
  • ATOMIC
  • UNDISPATCHED

だから、以下でも一緒

GlobalScope.launch(start = CoroutineStart.DEFAULT) {
        delay(1000L) 
        coroutineLog("World!")
    }

上記の場合はstart形式だけ指定できるけど、ドキュメントのlaunchにこんなんが kotlin.github.io

If the context does not have any dispatcher nor any other ContinuationInterceptor, then Dispatchers.Default is used.

contextにdispatcherとかContinuationInterceptorの情報持たせてへんかったら、Dispatchers.Default使うからよろしく!そんな感じのノリ

確かにCoroutineContext.common.ktに

internal expect fun createDefaultDispatcher(): CoroutineDispatcher

ってのがいたわ

で、こいつを辿ったらDispatchersっていうシングルトンがいたわ そこに定義されてるのが以下

  • Default
  • Main
  • Unconfined
  • IO

createDefaultDispatcher()がexpectで予期される実装~って言ってるのはプラットフォーム毎に使われるDispatchersを変える必要があるってことだと思います

リストで宣言されているものには、実際の~を表すactualが付与されていたので...(多分)

あ、でもIOだけはactualついてないです!!!

launchとかasync等のbuilder使えばDefaultやけど

* In order to work with `Main` dispatcher, following artifact should be added to project runtime dependencies:
     *  - `kotlinx-coroutines-android` for Android Main thread dispatcher
     *  - `kotlinx-coroutines-javafx` for JavaFx Application thread dispatcher
     *  - `kotlinx-coroutines-swing` for Swing EDT dispatcher

を見る感じ、MainはGUI絡みで使えよってことっぽい

Unconfinedはよくわかんないっす(まだexperimentalやから甘えた)

IOに関してもよくわかってないです

This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using
     * `withContext(Dispatchers.IO){}`

DefaultのDispatcherとthread shareするよー使うときはwithContext(Dispatchers.IO)で使えよってことなんでしょうかね?

この辺は追って追求で

Coroutineは軽量スレッド

ほんとか?

計測してみた

以下の5つのパターンを用意

// normal in coroutine ver
fun test1() = runBlocking {
    coroutineLog("test1 start")
    repeat(100_000) {
        coroutineLog(".")
    }
    coroutineLog("test1 end")
}

// coroutine ver
fun test2() = runBlocking {
    coroutineLog("test2 start")
    repeat(100_000) {
        launch {
            coroutineLog(".")
        }
    }
    coroutineLog("test2 end")
}

// thread in thread ver
fun test3() = runBlocking {
    coroutineLog("test3 start")
    repeat(100_000) {
        thread {
            println(".")
        }
    }
    coroutineLog("test3 end")
}

// normal
fun test4() {
    coroutineLog("test4 start")
    for (i in 1..100_000) {
        coroutineLog(".")
    }
    coroutineLog("test4 end")
}

// normal thread ver
fun test5() {
    coroutineLog("test5 start")
    for (i in 1..100_000) {
        thread {
            coroutineLog(".")
        }
    }
    coroutineLog("test5 end")
}

計測には measureTimeMillisを使ってます

結果

test1(normal in coroutine) end time: 598
test2(coroutine) end time: 937
test3(thread in coroutine) end time: 10945
test4(normal) end time: 348
test5(thread) end time: 9630

確かに軽量だった(約10倍くらいの差がある? )

Scope Builder

なんだこいつ???

runBlockingらのこと

ら??

coroutineScopeっていう独自のscope宣言ができるみたい

runBlockingとcoroutineScopeの違い
coroutineScopeはその中で生まれたcoroutineすべてが完了するまで、現在のスレッドをブロックしないとのこと

以下公式より

fun main() = runBlocking {
    launch {
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope {
        launch {
            delay(500L)
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope")
    }

    println("Coroutine scope is over")
}

結果

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

たしかにって感じやけど、今のところはよくわからんな

コルーチン内部処理を分離したい

これはシンプルで関数定義に修飾子suspendをつけてあげたらよい

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

よさそうですね

あれ?こういう例ならいいけど、現在のscopeを適用させてsuspend関数内でlaunchとか使いたい場合はどうしたらいいの?

一つの解決策はCoroutineScopeの拡張メソッドとして定義する

fun CoroutineScope.doWorld() = launch {
    delay(1000L)
    coroutineLog("World!")
}

でもこの解決策は常に使えるってわけじゃないんだぜ...もっといい方法あるよ!って言ってはる

以下、妄想解釈
classにCoroutineScopeを実装して、そのclassにcontext情報持たせて定義したい関数を用意するのがいいぜ!インスタンス生成時にJob生成してあげる(ライフサイクルあるならcreateとか)。したい処理が終わったりとか必要なくなったらちゃんと終了時にjob.cancel()しとけよ的な

肝心なcoroutineContextはカスタムバッキングプロパティでgetされたらjob返してあげたらいいよと

通常例

class Hello : CoroutineScope {
    private val job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = job

    fun hello() = repeat(10) {i ->
        launch {
            delay(500L)
            coroutineLog("hello!")
        }
    }

    fun destroy() {
        job.cancel()
    }
}

実はJobにはoperator plusが定義されている(なんだってー) てことはDispatcherを指定したかったら、+で結合するとよさそう

実装例

class Hello : CoroutineScope {
    private val job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Default

    fun hello() = repeat(10) {i ->
        launch {
            delay(500L)
            coroutineLog("hello!")
        }
    }

    fun destroy() {
        job.cancel()
    }
}

こゆこと???

async/await

以下にこういうコードがあったとしよう

fun main() = runBlocking<Unit> {
//sampleStart
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
//sampleEnd    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

結果

The answer is 42
Completed in 2017 ms

順番にone, twoって処理されてるってだけのやつですね

でも並列で処理したいよーーってときに登場するのがasync

fun main() = runBlocking<Unit> {
//sampleStart
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
//sampleEnd    
}

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

結果

The answer is 42
Completed in 1017 ms

重そうなとことか、非同期で任せたいとこをsuspendで分離して定義して、そこをasyncブロック内で呼び出すだけかな?分かりやすいし簡単そう

asyncはlaunchと同じような感じ
ただ、戻り値がDeferredになっているのだけ注意

one「こっから二手に分かれて分業しようぜー」
two 「おっけー」

one「待ち合わせ場所はawait()呼ばれたところでなー」

って感じでawaitしたとこで処理した結果を受け取る待ち合わせ場所になるっぽい

例えば、delayで片方だけ極端に遅くして待ち合わせ場所に片方がきても、ちゃんと待ってくれるみたい

わかりやすいですね

おじさん、時間ですよ

以上のここまでが時間の都合上書けたとこです...
もっと深ぼった内容は続きとして後日しっかりと書いていきたいと思います

  • エラーハンドリング
  • 実用例
  • キャンセル等
  • channel
  • select
  • immutable state and concurrency execute

とかとかまだいっぱいあるので実際に使いながら、勉強していきたいと思います!

詳細の実用例はkotlin confやandroid dev summitの動画をみて勉強してる真っ只中です(((((

以下リンク

www.youtube.com

www.youtube.com

www.youtube.com

あと、もしこの記事を見てくださった方で勉強してるよー!とかここの解釈はこうしたほうがいいなどあったら、どんどん言ってください!welcomeです!正しい知識を植え付けてください(((((

twitterなどで気軽に絡んでください~~~~ twitter.com

それではまた後日!!!!!