Slide 1

Slide 1 text

Shallow Dip into Kotlin Coroutine キクチコウダイ

Slide 2

Slide 2 text

自己紹介 菊池 広大(キクチコウダイ) 2023年6月 株式会社マネーフォワードに入社 埼玉出身、香港育ち、 Iターンで東京から福岡に引越し Github: https://github.com/BigBackBoom

Slide 3

Slide 3 text

発表の概要

Slide 4

Slide 4 text

Coroutineとは Coroutineが スレッドの並列処理とどう違うのか、 Kotlinをベースに説明します。

Slide 5

Slide 5 text

Coroutineとは

Slide 6

Slide 6 text

Coroutineとは サブルーチンを並列処理させるために 一般化した手法

Slide 7

Slide 7 text

Coroutineとは ● もともとは1963年にCobolのコンパイラ周りで提唱された ● 複数のサブルーチンが自分の処理を中断し、プロセス間で リソースの協力をしながら並列処理を実行する。

Slide 8

Slide 8 text

Coroutineとは

Slide 9

Slide 9 text

Coroutineとは 普通のサブルーチン(メソッド)とは違うの? a. サブルーチンは呼び出されるたびに、一から処理が開 始される b. Coroutineはサブルーチンで処理を中断して、別の処 理の開始・再開ができる

Slide 10

Slide 10 text

Coroutineとは スレッド使えば十分じゃない? ● スレッドはOS管理されている。そのため、OS毎に実装 を変更・バイナリの再コンパイルする必要がある。 ● また、スレッドを複数立ち上げると、ハードウェアリソー スも大量に消費する。

Slide 11

Slide 11 text

Kotlin Coroutineの使い方

Slide 12

Slide 12 text

Launch Lauchを呼び出すことでCoroutineの非同期 処理を開始できる fun main(args: Array) { GlobalScope.launch { println("world") } println("Hello") // 結果 // Hello } Launch

Slide 13

Slide 13 text

Launch ● Launch処理が始まる前にmainが終 わってしまうので変更 ● runBlockingは通常コードとCoroutine をブリッジするために使われる ● Globalscopeを直接指定するのは、 Warningが出るので注意 fun main(args: Array) { runBlocking { launch { println("Hello") } } println("World") // Hello // World } Launch

Slide 14

Slide 14 text

Launch ● Coroutine内でサブルーチンを呼び出 す ● サブルーチンの定義にsuspendをつけ る ● サブルーチンの呼び出しで処理の中断 が行われる suspend fun printWorld() { delay(1000) // 中断 println("world") } fun main(args: Array) { runBlocking { launch { printWorld() // 中断 } println("Hello") } // Hello // 〜1秒休憩〜 // World } } Suspend

Slide 15

Slide 15 text

Coroutineの実現方法

Slide 16

Slide 16 text

Coroutineの実現方法 Coroutineの処理はコンパイル時に ステートマシンコードが生成され置き換えられる。 単純にメソッドが実行されるわけではない

Slide 17

Slide 17 text

ステートマシン 右のコードをJVMバイトコードからデコンパイ ルして、中身を確認しました suspend fun printWorld() { delay(1000) // 中断 println("world") } fun main(args: Array) { runBlocking { launch { printWorld() // 中断 } println("Hello") // Hello // World }

Slide 18

Slide 18 text

ステートマシン デコンパイルした中身を擬似コード化 1. mainのLaunchの中身が実行される fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld() } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 19

Slide 19 text

ステートマシン デコンパイルした中身を擬似コード化 1. mainのLaunchの中身が実行される 2. 非同期なので、Helloがコンソールに表 示 fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld(this) } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 20

Slide 20 text

ステートマシン 3. launchのlabelの0が実行され、 printWorldのメソッドが動く fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld(this) } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 21

Slide 21 text

ステートマシン 3. launchのlabelの0が実行され、 printWorldのメソッドが動く 4. printWorldでDelayedが走り、 printWorldを再開先として、 ContinuationImplを渡す。 fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld(this) } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 22

Slide 22 text

ステートマシン 3. launchのlabelの0が実行され、 printWorldのメソッドが動く 4. printWorldでDelayedが走り、 printWorldを再開先として、 ContinuationImplを渡す。 5. returnされて一旦全体の処理が終了す る。 fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld(this) } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 23

Slide 23 text

ステートマシン 3. launchのlabelの0が実行され、 printWorldのメソッドが動く 4. printWorldでDelayedが走り、 printWorldを再開先として、 ContinuationImplを渡す。 5. returnされて一旦全体の処理が終了す る。 6. 1秒経つと、ContinuationImplの invokeSuspendが実行され、再度 printWorldを実行。 fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld(this) } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 24

Slide 24 text

ステートマシン 7. 二度目では、ラベルがマイナスに更新さ れているため、Worldがコンソールに表 示 fun printWorld(continue: Continuation) { val cont = ContinuationImpl() { fun invokeSuspend() { printWorld(this) } } switch(continue.label){ 0: label = 1 if(Delayed(1000, cont) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } println(“world") } // EventLoop // Default label = 0 fun main() { runBlocking { launch { switch(label){ 0: label = 1 if(printWorld(this) == suspend) return suspend break; 1: throwOnFailure 2: ThrowIllegalState } } println("Hello") } }

Slide 25

Slide 25 text

Coroutineの実現方法 中断の原理はステートマシンだったとして 並列処理はなんでできるの?

Slide 26

Slide 26 text

Coroutineの実現方法 Coroutineの実行周りは三つのクラスに分かれる ● Job:launch, async, flowなどのたびに作成される。 ● Worker:スレッドそのもの、Jobを実行する。 ● Scheduler:Workerを管理する。リソース管理を行う

Slide 27

Slide 27 text

Coroutineの実現方法

Slide 28

Slide 28 text

Coroutineの実現方法 ● 並列処理自体は、スレッドにて実現 ● 一つのスレッドが複数の処理を実行するので、処理ご とにスレッドを立ち上げるよりオーバーヘッドが少ない ● そのため、大量のスレッドを立ち上げるより軽量に動け る

Slide 29

Slide 29 text

Coroutineの実現方法 ● そのほかにも、JobがJobを作成していた場合の親子 関係や、実行できるスレッドのScopeやContextなどの 考えがある

Slide 30

Slide 30 text

Kotlin/Nativeの謎

Slide 31

Slide 31 text

Kotlin/Nativeの謎 Native周りのコードにlauchなどが見当たらない。

Slide 32

Slide 32 text

Kotlin/Nativeの謎 Kotlin → LLVM中間コードの コンパイラが見つからないので、 まさか直に置き換えている?🤔

Slide 33

Slide 33 text

Kotlin/Nativeの謎 それ以外の、Worker/Jobの概念は一緒。 Objective-C++で書かれているので、 iOS/Linuxなど複数プラットフォームをこれで対応して いる?

Slide 34

Slide 34 text

まとめ

Slide 35

Slide 35 text

Coroutineとは ● Coroutineは古くからある手法で、プログラミング言語の実 装として実現されている ● そのため、少ないリソースで非同期処理を実現している ● また、プログラミング言語さえ動けばOS環境は不問 ● Kotlinでは、JVMのスレッド処理とステートマシンによって 非同期な処理を実現している。

Slide 36

Slide 36 text

以上、ありがとうございました!

Slide 37

Slide 37 text

参考資料 ● Asynchronous Programming with coroutines