対象
- コルーチン全く知らないんだけど、一緒に勉強してくれる人がいない人
ひとりコードラボ会
ZOZOさんが、チームの開発力向上のためにコードラボ会をやっているとの情報を聞いたので、自分もやってみようと思いました。
コードラボは、Google様が提供している教育コンテンツのことです。
今まで、やってこなかったのですが、現場の方も活用しているということは、情報の質が高いのかなと思い、チラ見したらそんな感じしたので、習慣化しようと思いました。
コルーチンの概要
「コルーチンの概要」というコンテンツをやろうと思います。
超入門レベルな解説いきます。
スレッドとは
fun main() {
println(a())
println(b())
println(c())
}
fun a() = "a"
fun b() = "b"
fun c() = "c"
Androidプログラミング(多分他もそう)は、基本的にメインスレッド君が一人で処理を行っています。
スレッドとは、プログラミングにおいて処理の始まりから終わりまでの一連を一塊とした単位のことです。
が、まぁスレッドという人が処理していると捉えたほうが、イメージ化しやすいと思います。
上記のような感じの処理があるとすると、逐次処理なので a -> b -> c の順に処理されていくわけです。
仮に一つ一つの処理に、1ミリ秒かかるとすると計3ミリ秒となります。
しかし、bが1時間かかるとしましょう。
fun main() {
println(a())
println(b())
println(c())
}
fun a() = "a"
fun b() = {
// 何かしらで1時間かかる重い処理
}
fun c() = "c"
}
メインスレッドくんが、一人で処理することになるため計1時間と2ミリ秒かかってしまいます。
実際のアプリでは、c にViewの更新があると画面が謎に固まるというウザい現象になり、ユーザーが離れてしまいます。
それに関しては、もう一人の分身を作ってそいつにやらせるが解決策です。
メインスレッドが「本体」で、他のスレッドが「分身」というイメージです。
Threadインスタンスを生成することで、分身の出来上がりです。
ラムダ内に、処理を書けば分身がそれをやってくれます。
これによって、bが処理されるまで、c処理が待ち状態になること(俗に言うブロッキング)がなくなります。
役割分担はこんな感じ。
本体: a -> c
分身: b
fun main() {
println(a())
Thread { println(b())}.start()
println(c())
}
fun a() = "a"
fun b() = {
// 何かしらで1時間かかる重い処理
}
fun c() = "c"
}
また、分身は複数作ることができます。
出力結果も添付されているので、コードラボの方をコピペしやす。
for文で分身を作るのを3回回している感じです。
fun main() {
val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
repeat(3) {
Thread {
println("${Thread.currentThread()} has started")
for (i in states) {
println("${Thread.currentThread()} - $i")
Thread.sleep(50)
}
}.start()
}
}
そして、出力結果は下記のようになるみたいです。
currentThread()は、スレッドの詳細を出力するので、この場合ですと
[番号、優先度、スレッド]だと思います。(間違っていたらすまぬ)
Thread[Thread-0,5,main] has started
Thread[Thread-1,5,main] has started
Thread[Thread-2,5,main] has started
Thread[Thread-1,5,main] - Starting
Thread[Thread-0,5,main] - Starting
Thread[Thread-2,5,main] - Starting
Thread[Thread-1,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 1
Thread[Thread-2,5,main] - Doing Task 1
Thread[Thread-0,5,main] - Doing Task 2
Thread[Thread-1,5,main] - Doing Task 2
Thread[Thread-2,5,main] - Doing Task 2
Thread[Thread-0,5,main] - Ending
Thread[Thread-2,5,main] - Ending
Thread[Thread-1,5,main] - Ending
このように、Thread1 -> Thread2-> Thread3 -> と順々に処理されているのではなく、分身が各々処理を進めているのがわかると思います。
これが、スレッドの特徴です。
うまく活用することで、処理の時間を短くしたり、同時にいろんな処理を行うことができます。
スレッドの課題
物事には良い面もあれば、悪い面もあるので、スレッドにもいくつか問題点があります。
- OS消費が多め
スレッドを作成や切り替えること諸々消費が激しいです。
アプリは、最低限一秒間に60フレームのスピード感で更新が行われます。
スレッド管理が多すぎると応答性が悪くなってしまう場合があります。
- 予期せぬ動作が起きることも
マルチスレッドの仕組みがややこしいので、ふんわりとした説明になります。(やはり、エンジニアにCSの教養は必要なわけですw)
プロセッサという、CPUに命令を指揮する部分が分身に処理を分け与えても、そいつが処理を始めるタイミングや途中で中断するタイミング等は制御不能なんです。
そのため、スレッドを直で扱おうにも想定した挙動にすることは難しいという問題点があります。
コードラボには、その例が掲載されています。
fun main() {
var count = 0
for (i in 1..50) {
Thread {
count += 1
println("Thread: $i count: $count")
}.start()
}
}
下記にてスレッド番号を見ると分かるように、ランダムな処理になってしまいます。
これが、予期せぬ動作が起き得るという問題点です。
Thread: 50 count: 49 Thread: 43 count: 50 Thread: 1 count: 1
Thread: 2 count: 2
Thread: 3 count: 3
Thread: 4 count: 4
Thread: 5 count: 5
Thread: 6 count: 6
Thread: 7 count: 7
Thread: 8 count: 8
Thread: 9 count: 9
Thread: 10 count: 10
Thread: 11 count: 11
Thread: 12 count: 12
Thread: 13 count: 13
Thread: 14 count: 14
Thread: 15 count: 15
Thread: 16 count: 16
Thread: 17 count: 17
Thread: 18 count: 18
Thread: 19 count: 19
Thread: 20 count: 20
Thread: 21 count: 21
Thread: 23 count: 22
Thread: 22 count: 23
Thread: 24 count: 24
Thread: 25 count: 25
Thread: 26 count: 26
Thread: 27 count: 27
Thread: 30 count: 28
Thread: 28 count: 29
Thread: 29 count: 41
Thread: 40 count: 41
Thread: 39 count: 41
Thread: 41 count: 41
Thread: 38 count: 41
Thread: 37 count: 41
Thread: 35 count: 41
Thread: 33 count: 41
Thread: 36 count: 41
Thread: 34 count: 41
Thread: 31 count: 41
Thread: 32 count: 41
Thread: 44 count: 42
Thread: 46 count: 43
Thread: 45 count: 44
Thread: 47 count: 45
Thread: 48 count: 46
Thread: 42 count: 47
Thread: 49 count: 48
また、複数のスレッドを走らせると同じメモリ領域に同時アクセスなんかも起こりうるので、クラッシュの原因になります。
さらに、スレッドが順不同に処理されることによるバグは再現はほぼできないので、クラッシュの原因を特定するのは極めて困難です。
スレッドを手動で操作するのは良くないんですね。
コルーチンとは
そんなマルチスレッドによる問題を回避するためにコルーチンを使います。
まずは、簡単にコルーチンのメリデメについて解説していきます。
メリット
- 保存
分身が処理しているのを一旦停止して、保存することが可能です。
それにより、途中から別のスレッドで処理を再開することも出来ます。
これのことを通称「軽量」と言われています。
まぁ、確かにスレッド間で処理の持ち運びができるので、軽いイメージはある。(けど、いきなり軽量って言われても?ってなる。)
コルーチンは特殊な分身(≒スレッド)です。
厳密にはスレッドではないが、役割としてはスレッドと同じ。
- 短い
短いコードで簡潔に書く事が出来ます。
- 節約
構造化された同時実行とやらでメモリリークの削減になる(ここの掘り下げ先延ばし)
- 相性
Jetpackライブラリのと相性が良いです。
コルーチンをサポートしているので!
デメリット
- 書き方が色々あり過ぎる
調べていて、色んな書き方があり混乱する瞬間がいくつかあった笑。
スレッドの操作は簡単にできるけど、どれが良い書き方なのか結局分からない。
コルーチンの使い方
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
最初の方に書いた処理をコルーチンで走らせると、下記のようになります。
fun main() = runBlocking {
launch { // このスコープ内で、新しい分身(コルーチン)が作成され、処理が走る。
b()
}
// 下記がコルーチンのメインスレッド
a()
c()
}
fun a() = "a"
fun b() = {
// 何かしらで1時間かかる重い処理
}
fun c() = "c"
}
launch 「作成」
新しい分身(コルーチン)を作成を行います。
launchメソッドの裏コードはこんな感じです。
blockプロパティにsuspendキーワードがついているので、スコープ内の処理を一時中断できる事が分かります。
fun CoroutineScope.launch {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
}
runBlcking 「中断」
こちらもコルーチンの生成を担っているのですが、コルーチンを用いてないコードと用いているコードの仲介役を担います。
このスコープ内(CoroutineScope)でないとlaunchを正しく呼び出すことが出来ません。
とはいえ、runBlockingはスコープ内の処理が終わるまで、現在のスレッドがブロッキングされます。
アプリケーションスコープで使われるので、実用性は皆無ないようです。(なぜサンプルで使った!?)
その他
Dispathers 「決定」
どのスレッド(分身)で実行するか決定します。
GlobalScope 「全体」
アプリ単位でコルーチンを作成するためのものです。
メモリリークに繋がるので、使いどころに気をつける必要があります。
不明点
さくっと終わらせたいので、一先ずこのくらいにします。
解消できなかった、不明点はこちら。
- suspendについて
- 構造化された同時実行について
- ざっくりとではあるが、スレッドの仕組み(読み進めていく中で、自分が思っているスレッドのイメージと合ったり合わなかったりで違和感が残った。)
終わりに
スレッドの仕組みがいまいち良く分からないので、こんなもんかなという感じで書きました。
初めてのコードラボですが、とっても質の高い情報で読みごたえがありました。
これを継続させていけば、確かにAndroidの知識は底上げできるなと思います。
今回は、あくまでも概要でコードを書くテーマではなかったので、次はコルーチンを書き書きするのをやりたいと思います。