たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 43708
目次
本題
環境
はじめに
あらずし
ライブラリの解説
コールバック
なぜコールバック
コールバックの代替案 Future と Promise
ついに来た async / await や Kotlin Coroutines
最初のコルーチン
大量にコルーチンを起動できる理由 その1
付録 譲るところを見てみる
付録 Thread.sleep と delay
大量にコルーチンを起動できる理由 その2
付録 本当にメモリ使用量が少ないのか
スレッドでテスト
コルーチンでテスト
構造化された並行性
launch が使えない問題
親は子を見守る
エラーが伝搬する
ちなみに
キャンセル
コルーチンのキャンセル方法
コルーチンスコープを利用したキャンセル
Android のコルーチンスコープのキャンセル
コルーチンが終わるまで待つ
キャンセルの仕組み
try-catch / runCatching には注意
キャンセルは協力的
キャンセル可能な処理の作り方
付録 どこに ensureActive() / isActive を入れるの
付録 ensureActive() と isActive 2つもあって迷っちゃうな~
付録 yield() の説明もしろ
try-finally が動く
finally でコルーチンが起動できない解決策
タイムアウト
付録 質問:キャンセル例外を投げればキャンセルしたことになりますか
サスペンド関数
直列処理
並列処理
並列処理の開始を制御する
構造化された並行性
付録 launch と async
コルーチンコンテキストとディスパッチャ
スレッドとディスパッチャ
Android が用意しているスコープはメインスレッド
付録 Dispatchers.Main.immediate の immediate って何
newSingleThreadContext
Unconfined
コンテキストを切り替える withContext
Job()
デバッグ用に命名
同時に Dispatchers NonCancellable Job を指定したい
コルーチンスコープ
スレッドローカルデータ
付録 Android の Handler と Dispatchers
付録 Dispatchers.Default はなぜ最低 2 個で、Dispatchers.IO は最低 64 個もあるの
Flow と Channel
例外
違う親の子コルーチンの例外
キャッチされなかった例外を観測
キャンセルと大元の例外
複数の例外の報告
スーパーパイザー
並行処理と可変変数
マルチスレッド起因の問題はコルーチンでも起きる
ループと withContext
スレッドセーフ
以上!
ファンディスク
コールバックの関数をサスペンド関数に変換する
並列と並行(パラレルとコンカレント)
平行(コンカレント)
並列(パラレル)
だから何?
Mutex
Semaphore
limitedParallelism
forEach / map でサスペンド関数呼べます
追記 2025/08/27 ファンディスク2
awaitCancellation
invokeOnCompletion
おわりに
どうもこんばんわ。
あまいろショコラータ1・2・3 コンプリートパックを買ってやっています。買ったのは3の発売のときだったので積んでたことになりますね、、
まずは、あまいろショコラータ攻略しました。
みくりちゃん!!!ところどころにあるやりとりがおもしろかった
↑じとめすち
むくれてるのかわい~
英語分からん分かる、英語が第一言語だとやっぱドキュメントもエラーメッセージもそのまま分かるのでしょうか。
直でドキュメント読めるのずるいとおもう(?)
><
それはそうと服が似合ってていい
あとあんまり関係ないけど無印版のエンディング曲がシリーズの中で一番好きかもしれません、
あ!!!!!これ
このブログの土日祝日のアクセス数のこと言ってますか!??!?!?!
技術(?)ブログ、休みの日はあんまりお客さん来てない。CloudFrontの転送量をCloudWatchで見てみたけど、明らかに休みの日だけ折れ線グラフがガタ落ちしてる。面白い。
Jetpack Composeがぶいぶい言わせている今日、Jetpack ComposeではありとあらゆるところでKotlin Coroutinesのサスペンド関数や、Flowが多用されています。
(Androidはかなり)コールバック地獄だったので、他の言語にあるような同期スタイルで記述でき、とても嬉しいわけですが、、、、
Android本家のKotlin Coroutinesの紹介がおざなりというか、分かる人向けにしか書いていません!(別にAndroid側が紹介する義理なんて無いけど)Jetpack Composeであれだけ多用しているのに!?!?(いや関係ないだろ)
Kotlin coroutines on Android | Android Developers
https://developer.android.com/kotlin/coroutines
Kotlin Coroutinesのドキュメントを読んでみようの会です。ぜひ夏休みの読書感想文にどうぞ。(てか世間は夏休みなのか)
Coroutines guide | Kotlin
https://kotlinlang.org/docs/coroutines-guide.html
Kotlin Coroutinesの話をするのに、Androidである必要はないんですが、私がサンプルコード書くのが楽というだけです。
特別なことがなければAndroidじゃない環境、Kotlin/JVMとかでも転用できます。
いちおうドキュメントの流れに沿って読んでいこうかなと思うのですが、
スレッドと違い、なぜコルーチンは欲しいときに欲しいだけ作っていいのかとかの話をしないと興味がわかないかなと思いそこだけ先にします。
多分合ってると思う、間違ってたらごめん。
まあ分かりやすいと思うのでAndroidで、あとはJetpack Composeを使います。
もしサンプルコードを動かすなら↓
サンプルとしてインターネット通信が挙げられると思うので、OkHttpというHTTP クライアントライブラリも入れます。
インターネット通信のサンプルがあるため、インターネット権限を付与したプロジェクト(アプリ)を作っておいてください。
FutureとかPromiseとかasync / awaitを知っていればもっと分かりやすいかもしれませんが知らなくてもいいです。なんなら忘れても良いです。
というのもFutureやPromiseには無い安全設計が存在したり、Kotlin Coroutines は他の言語にある async/awaitという回答は大体あっているくらいしかないです。
また、Kotlin Coroutinesではasync/awaitは並列実行のために使われており、他の言語にあるasync functionに当たるものはsuspend funになります。
一応言っておくと別にスレッドとかFutureとかPromiseの悪口が言いたいわけじゃないです。
これはコルーチンとは関係ないのですが、OkHttpというライブラリのコードでサンプルを書くので、
最低限の使い方をば。
これが非同期モード。現在のスレッドはブロックせず、代わりに通信結果はonResponse()受け取る。
ブロックしないためメインスレッド(UIスレッド)からも呼び出せます。
val request1 = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request1).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// エラー
}
override fun onResponse(call: Call, response: Response) {
// 成功
}
})これが同期モード。execute()を呼び出すと、レスポンスが返ってくるまで現在のスレッドをブロックします。
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
val response = OkHttpClient().newCall(request).execute()Androidでコルーチンが来る前、コールバックをよく使っていました、てかこれからも使うと思います。Javaで書く人たちはKotlin Coroutines使えないので。
人によってはRxなんとかを使ってたりしたそうですが、Androidライブラリのほとんどはコールバックだったと思います。
かくいうAndroidチームが作るライブラリAndroid Jetpack(androidx.hogehoge みたいなやつ)も、もっぱらそう。
コールバックは、せっかく覚えたプログラミングのいろはを全部台無しにしていきました。
というわけでまずはコールバックの振り返り。OkHttpってライブラリを入れてサンプルを書いていますが、Kotlin Coroutinesあんまり関係ないのでここは真似しなくてもいいと思います。
例えばプログラミングでは、上から処理が実行されるという話がされるはず、でも意地悪してこれだとどうなるかというと。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
println("1 番目")
val request = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
println("2 番目")
}
})
println("3 番目")
}
}Logcatの表示だとこれ。全然上から順番に処理されてない。
1 番目
3 番目
2 番目forも使える、、あれ?コールバックだと期待通りじゃない?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
repeat(10) { index ->
println("Request $index")
val request = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
println("onResponse $index")
}
})
}
}
}Logcatが途中までは良かったのに、順番がぐちゃぐちゃになってしまった。
Request 0
Request 1
Request 2
Request 3
Request 4
Request 5
Request 6
Request 7
Request 8
Request 9
onResponse 1
onResponse 8
onResponse 7
onResponse 5
onResponse 3
onResponse 2
onResponse 0
onResponse 6
onResponse 9
onResponse 4順番を守るためには、コールバックの中に処理を書かないといけないわけですが、見通しが悪すぎる。
俗に言うコールバック地獄。Androidはこんなのばっか。Camera2 APIみてるか~?
val request1 = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request1).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// エラー1
}
override fun onResponse(call: Call, response: Response) {
// 成功1
val request2 = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request2).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// エラー2
}
override fun onResponse(call: Call, response: Response) {
// 成功2
}
})
}
})例外処理のtry-catchを覚えたって?コールバックの前では役立たずです。
成功時に呼ばれる関数、失敗時に呼ばれる関数に分離。finallyが欲しい?関数作って両方で呼び出せばいいんじゃない?
val request = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("失敗時")
}
override fun onResponse(call: Call, response: Response) {
println("成功時")
}
})それ以前に、コールバックがなければ直接スレッドを作って使うしか無いのですが、これは多分あんまりないと思います。
そもそもAndroidだとコールバックのAPIしか無いとかで。
それでもなおコールバックを使っていたのには、わけがちゃんとあります。
画面が固まってしまうんですよね。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// UI スレッドから呼び出してみる
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
// インターネット通信は時間がかかる
// 通信制限されていたらいつ処理が終わるか分からない。execute() がいつ返り値を返すか分からない。返すまでは画面が固まってしまう。
val response = OkHttpClient().newCall(request).execute()
// Toast を出す
Toast.makeText(this, "こんにちは $response", Toast.LENGTH_SHORT).show()
}
}最初からあるスレッド、UIスレッドやメインスレッドとも呼ばれていますが、これは特別で、UIの更新や入力を受け付けるものになります。UIの処理はこれ以外の他のスレッドでは出来ません。
このスレッドで軽い処理なら問題ないでしょうが、インターネット通信(しかも通信制限のユーザーが居るかも知れない!)をやってしまったら、しばらく画面が固まってしまいます。押しても反応しないアプリが誕生します。
やがて行き着く先はこのダイアログ、「アプリ名」は応答していません。これはメインスレッドで行われている処理に時間かかった際に出ます。Android 4.xあたりまではよく見た記憶がある。今でもメインスレッドを止めればいつでもこのダイアログに会えます。
これを回避するためにスレッドを使ったり、コールバックを使い、時間がかかる処理をメインスレッドで行わないようにしていたわけです。
まあそれ以前に Android ではメインスレッドでインターネット通信できない(例外が投げられる)ので、そもそもやりたくても出来ません。
これで、アプリの安定性が上がった。 アプリがフリーズしないように対策出来た。代償としてコードが地獄になったわけ。つらい。
アプリの安定性からアプリがフリーズしないように対策とわざわざ言い直したわけですが、コールバックやスレッドだって使えば安定するかと言われると、そう簡単には安定しません。
ちゃんと使わないと別のエラーで落ちます。こっちは落ちます。フリーズじゃなくて。これが厄介!
Android 11で非推奨になったAsyncTaskで痛い目を見た。なんて懐かしいですね。なんなら全然うまく動かなくてトラウマになってる人もいそう。私ももう見たくない。
画面回転したら落ちるとか、アプリから離れると落ちるとか、、、
流石にそれはしんどいのでMVVM的な考え方にのっとり、UIを担当する処理とそれ以外が分離されました。
画面回転ごときで落ちるのは、UIの部分で通信処理を書いているのが原因。ViewModelというUIに提供するデータを用意する場所で書けばいい。こいつは画面回転を超えて生き残る。
コールバックが来ようと、同期的な処理になろうとずいぶんマシになったはず。
話がちょっとそれちゃったけど、これが今日のAndroidで、まあ後半はKotlin Coroutinesあんまり関係ないんですが、非同期とかコールバックがしんどいってのがわかれば。
Rxなんとか←これは使ったこと無いので触れないです。すいません。Futureも名前知ってるレベルであんまり知らないです。Promiseがちょっとだけ分かります。
Promise(ゲッダンの方ではない)とか
Future(FeatureじゃなくFuture)というのがあります。
コールバックの代替案で、PromiseはJavaScriptではasync/awaitとともに使われています。
Androidの話なので、JavaScriptの話をしてもあれですが、一応ね。こんな感じのJavaScriptです。Promiseはコールバックのように非同期で処理されます。そのためどこかで待つ必要があります。then()とcatch()ですね。
onSuccess / onResponse / onError / onFailure とか?)、Promiseで書かれていればthen() / catch()と言った感じで一貫しています。Promise 本に書いてありました。→ 
JavaScript Promiseの本
JavaScriptのPromiseを使った非同期処理の書き方、テスト、アンチパターンについて解説した無料の電子書籍
https://azu.github.io/promises-book/
then()では、配列操作のmap { }のように値を変換して返すことが出来ます。
ここにPromiseを返すことが出来て、このあとのthen()で受け取ることが出来ます。Promiseで出来ていれば一貫していることになるので、処理を繋げることが出来ます。
これをPromise チェーンとか言うそうです。
function main() {
fetch("https://example.com") // Promise を返す HTTP クライアント。Response を返す Proimse です
.then(res => res.text()) // Promise 経由で Response を受け取り、Response.text() を返す。String を返す Promise です
.then(text => console.log(text)) // Response.text() の Promise 結果を受け取る
.catch(err => console.log(err)) // エラー
}Promise チェーンが無いとコールバック地獄になってしまいますからね。
↓のコードは↑のコードと大体同じですが、明らかに↑の、メソッドチェーンで呼び出していくほうがまだマシでしょう。
function main2() {
// コールバックが深くなっていく
// マトリョーシカ
fetch("https://example.com").then(res => {
res.text().then(text => {
console.log(text)
})
})
}Kotlin Coroutinesの話をするのであんまり触れませんが、あとは複数のPromiseを待ち合わせたりも出来ます。
コールバックだと全部のコールバックが呼ばれたかの処理が冗長になりそうですからね。
function main3() {
const multipleRequest = [
fetch("https://example.com"),
fetch("https://example.com"),
fetch("https://example.com")
]
// Promise 全部まつ
Promise.all(multipleRequest).then(resList => {
// resList は Response の配列
})
}ただ、よく見るとコールバックが少し減ったくらいしか差が無いと言うか(申し訳ない)、Promiseを繋げたり、全部待つ必要がないならあんまり旨味がないのでは?と。then()で受け取るのとコールバックで受け取るのはあんまり差がない?。then() / catch()のせいでtry-catchは結局使えないじゃんって?。
上記のPromiseではまだコールバック風な文化が残っていました。しかしついに今までの同期風に書けるような機能が来ました。async/awaitです。async/awaitはそれぞれえいしんく/えいうぇいとと読むらしい、あしんく/あうぇいとってのは多分違う。
ついにtry-catchが使えるようになりました。
同期風にかけるようになったため、Promiseを繋げる→ああPromise チェーンかとか考えなくても、awaitを付けてあとは同期的に書けば良くなりました。
async function main4() {
try {
const response = await fetch("https://example.com") // Promise が終わるまで待つ
const text = await response.text()
console.log("成功")
} catch (e) {
console.log("失敗")
}
}
function main5() {
main4() // async function は Promise を返す
.then(() => { /* do nothing */ })
.catch(() => { /* do nothing */ })
}ループだって怖くない。ただしJavaScriptはforEach() / map()でasync function呼び出せないので、純粋なループにする必要あり。
async function main4() {
try {
for (let i = 0; i < 10; i++){
const response = await fetch("https://example.com") // Promise が終わるまで待つ
const text = await response.text()
console.log(`成功 ${i}`)
}
} catch (e) {
console.log("失敗")
}
}
function main5() {
main4() // async function は Promise を返す
.then(() => { /* do nothing */ })
.catch(() => { /* do nothing */ })
}
main5();ちゃんとforの順番通りでました!もうコールバックやだ!
成功 0
成功 1
成功 2
成功 3
成功 4
成功 5
成功 6
成功 7
成功 8
成功 9Kotlin CoroutinesはこれのKotlin版です。async functionはsuspend funになります。
というわけで長い長いあらずしも終わり。いよいよ本題にいきましょう。
兎にも角にもなにか書いてみましょう。というわけでこちら。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch { // コルーチンを起動
delay(1000L) // 1秒間コルーチンを一時停止
println("World!") // 待った後に println
}
println("Hello") // 予想通り、スレッドのときと同じくまずはこれが出力されます。
}
}"Hello"を出力して、その1秒後に"World"を出力するコルーチンです。
出力結果はこう。何の面白みもないですが。
Hello
World!launch { }関数を使い新しいコルーチンを起動しています。delay()は指定時間コルーチンの処理を一時停止してくれます。
lifecycleScopeというのは、AndroidのActivityと連携したコルーチンスコープです。コルーチンスコープというのは後で説明しますが、とにかく新しくコルーチンを起動するときにはスコープが必要だということがわかれば。
ちなみに、ドキュメントではGlobalScopeや、runBlocking { }がコード例として出てきますが、Androidアプリ開発ではまず使いません。Androidでコルーチンを使う場合は、lifecycleScopeとかrememberCoroutineScope()とかの用意されたコルーチンスコープを使い起動します。コルーチンスコープを自分で作ることも出来ますがあんまりないと思います。
GlobalScopeとrunBlocking { }はコルーチンのサンプルコードを書く場合には便利なのですが、実際のコードの場合は少なくともAndroidでは出番がありません。
delay()はサスペンド関数の1つで、fun start()をsuspend fun start()のように書き直せば、サスペンド関数を自分で作ることも出来ます。
これらはサスペンド関数の中で呼び出すか、launch { }、async { }などの中じゃないと呼び出せません。
自分でsuspend funを作った場合、上記のコードはこんな感じになります。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch { // コルーチンを起動
printWorld() // suspend fun を呼ぶ
}
println("Hello") // 予想通り、スレッドのときと同じくまずはこれが出力されます。
}
private suspend fun printWorld() {
delay(1000L) // 1秒間コルーチンを一時停止
println("World!") // 待った後に println
}
}ザックリ説明したところで、
コルーチンの話をしていく前に、先に言った通り、なぜスレッドと違って大量にコルーチンを作れるのかという話を。
Kotlin Coroutinesは今あるスレッドを有効活用します。スレッドを退屈させない(遊ばせない)仕組みがあります。
例えば以下のコード。3秒後と5秒後にprintlnするわけですが、これを処理するのにスレッドが 2 個必要でしょうか?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
delay(5_000)
println("5 秒待った")
}
lifecycleScope.launch {
delay(3_000)
println("3 秒待った")
}
println("起動した")
}
}例えばその下の3秒後に printlnとか5秒間待ってる間に処理できそうじゃないですか?
lifecycleScope.launch {
delay(5_000) // この 5 秒待ってる間に、その下の 3 秒後に println するコードが動かせるのでは、、、?
println("5 秒待った")
}
lifecycleScope.launch {
delay(3_000)
println("3 秒待った")
}
println("起動した")Kotlin Coroutinesはこんな感じに、待ち時間が発生すれば、他のコルーチンの処理をするためスレッドを譲るようにします。
これにより、launch { }した回数よりも遥かに少ないスレッドで処理できちゃうわけ。
一時停止と再開という単語がでてきますがおそらくこれです。
実際に譲っているか見てみましょう。printlnを追加して、どのスレッドで処理しているかを出力するように書き換えました。Dispatchers.Defaultというまだ習ってないものを使ってますが、とりあえずは別のスレッドを使う場合はこれをつければいいんだって思ってくれれば。Dispatchersで詳しく話します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
println("[launch 1] 起動 ${Thread.currentThread().name}")
delay(5_000)
println("[launch 1] 5 秒待った ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Default) {
println("[launch 2] 起動 ${Thread.currentThread().name}")
delay(3_000)
println("[launch 2] 3 秒待った ${Thread.currentThread().name}")
}
println("起動した")
}
}結果です。実行する度に若干変化するかと思いますが、私の手元ではこんな感じでした。launch { }直後はそれぞれ別のスレッドが使われてますが、delay後は同じスレッドを使っている結果になりました。
ちゃんとdelayで待っている間、他のコルーチンにスレッドを譲っているのが確認できました。
[launch 1] 起動 DefaultDispatcher-worker-1
起動した
[launch 2] 起動 DefaultDispatcher-worker-2
[launch 2] 3 秒待った DefaultDispatcher-worker-1
[launch 1] 5 秒待った DefaultDispatcher-worker-1また、この結果を見るに、delay()する前と、した後では違うスレッドが使われる場合がある。 ということも分かりましたね。メインスレッドの場合は 1 つしか無いので有りえませんが、このサンプルではDispatchers.Defaultを指定したためにこうなりました。
詳しくはDispatchersのところで話しますが、スレッドこそ違うスレッドが割り当てられますが、Dispatchers.Defaultは複数のスレッドを雇っているので、Defaultの中で手が空いているスレッドが代わりに対応しただけです。
(また、これはdelay()に限らないんですがまだ習ってないので・・・)
この2つ、どちらも処理を指定時間止めてくれるものですが、大きな違いがあります。
Thread.sleepはスレッド自体を止めてしまいます。コルーチンを実際に処理していくスレッド自体が止まってしまいます。
一方delayは指定時間コルーチンの処理が一時停止するだけで、スレッドは止まらない。止まらないので、待っている間、スレッドは他のコルーチンの利用に割り当てることができます。
delayはスレッドが止まるわけじゃないので、例えばメインスレッドで処理されるコルーチンを作ったところで、ANRのダイアログは出ません。コルーチンが一時停止するだけで、メインスレッド自体は動き続けていますから。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Main) { // Main でメインスレッドで処理されるコルーチンが作れます
delay(10_000) // Thread.sleep で 10 秒止めたら確実に ANR ですが、delay はコルーチンが一時停止するだけなので
Toast.makeText(this@MainActivity, "ANR は出ません!", Toast.LENGTH_SHORT).show()
}
}
}たとえ無限にdelayしたとしても、上記の理由によりびくともしないと思います。
ただ、上記の説明は、既存のスレッドを効率良く使う説明であって、
大量に作ってもいい理由にはあんまりなっていない気がします。
Javaのスレッドは、OSのスレッドを使って作られています。JavaのスレッドとOSのスレッドは1:1の関係になりますね。
スレッドを新たに作るのはメモリを消費したりと、コストがかかる処理のようです。
また、搭載しているCPUのコア数以上にスレッドを生成されると処理しきれなくなるため、各スレッド均等に処理できるよう(よく知らない)コンテキストスイッチと呼ばれる切り替えるための仕組みがあるのですが、これも結構重い処理らしい。
どうでも良いですが、コンテキストスイッチがあるので、1コア CPUだとしても複数のスレッドを動かすことが出来ます。同時に処理できるとは言ってませんが。(パラレルとコンカレントの話は後でします)
話を戻して、Kotlin Coroutinesはlaunch { }でコルーチンを作成しても、スレッドは作成されません。
もちろん、 コルーチンの中身を処理していくスレッドが必要なのですが、Kotlin Coroutines側ですでにスレッドを確保しているので(詳しくはDispatchersで)、それらが使われます。
そのためコルーチンと、確保しているスレッドの関係は多:多の関係になります。
どれかのスレッドで処理されるのは確かにそうですが、スレッドとコルーチンが1:1で紐付けられるわけではありません。大量にコルーチンを起動出来るもう一つの理由ですね。
コンテキストスイッチに関してもOSのスレッドだとOSがやるので重たい処理になる(らしい)のですが、
コルーチンだとKotlin側が持ってるスレッド上でコルーチンを切り替えるだけなので軽いらしい。
そうは言ってもよく分からないと思うので、thread { }が本当に重たいのか見ていきたいと思います。
それぞれ1000個(!?)作ってみます。Pixel 8 Pro / Android 15 Betaで試しました。デバッグビルドなのであんまりあてにならないかも。
開発中のアプリであれば、Android StudioのProfilerでメモリ使用量を見ることが出来ます。
ボタンを押したらスレッドを作ってThread.sleep(60秒)するコードを書きました。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
KotlinCoroutiensPracticeTheme {
MainScreen()
}
}
}
}
@Composable
private fun MainScreen() {
fun runMemoryTest() {
repeat(1000) {
thread {
Thread.sleep(60_000)
}
}
}
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Button(onClick = { runMemoryTest() }) {
Text(text = "確認")
}
}
}
}結果がコレです。
ボタンを押したら赤い丸ポチが付くわけですが、まあ確かに増えてますね。
ボタンを押したら、コルーチンを起動(launch { })して、delay(60秒)するコードを書きました。みなさんも試してみてください。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
KotlinCoroutiensPracticeTheme {
MainScreen()
}
}
}
}
@Composable
private fun MainScreen() {
val scope = rememberCoroutineScope()
fun runMemoryTest() {
repeat(1000) {
scope.launch {
delay(60_000)
}
}
}
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Button(onClick = { runMemoryTest() }) {
Text(text = "確認")
}
}
}
}結果がコレで、赤い丸ポチが出ているときがボタンを押したときです。
最初ちょっと増えましたが、2回目以降は押しても得には、目に見えるレベルで増えたりはしてなさそう。
実際にJavaのスレッドを作っているわけじゃないだけあって、いっぱい押しても特に起きない
コルーチンのドキュメントをいい加減なぞっていこうかと思ったのですが、
もう一個、これはスレッド、Future、Promiseから来た人たちが困惑しないように先に言及することにしました。これら3つにはない考え方です。
英語だとstructured concurrencyって言うそうです。かっこいい。
もしこれを見る前にすでにKotlin Coroutinesを書いたことがある場合、まず壁にぶち当たるのがこれ。launch { }を使いたくても、赤くエラーになるんだけど。って。しかも謎なことに、書く場所によってはエラーにならない。一体なぜ!?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
downloadFile()
launch { } // ここはエラーじゃない(?)
}
}
private suspend fun downloadFile() {
launch { } // ここだとエラー(???)コルーチン起動したいよ!
}
}エラーになるので仕方なくGlobalScopeと呼ばれる、どこでも使えるコルーチンスコープを使って強行突破を試みますが、
そもそもGlobalScopeを使うことが滅多にないとLintに警告されます。なんでよ!?
private suspend fun downloadFile() {
GlobalScope.launch { } // GlobalScope は滅多に使いません。
}GlobalScopeのドキュメントを見に行きましょう。読んでみると、どうやらcoroutineScope { }を使えば、suspend funの中でもlaunch { }出来るっぽいですよ!?
試してみると、確かにcoroutineScope { }ではエラーが消えています。
private suspend fun downloadFile() {
coroutineScope {
launch { } // 祝!これで起動できた!!!
}
}launch { }がcoroutineScopeの拡張関数になっているからですね。fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Jobまた、launch { }の中でlaunch { }出来たのは、launchの引数blockが、thisでCoroutineScopeを提供していたからなんですね。
以下のコードが分かりやすいかな?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
val scope: CoroutineScope = this // launch のブロック内はコルーチンスコープがある
this.launch { } // コルーチンスコープがあるので起動できる
launch { } // これでいい
}
}
}ちなみに、launchがCoroutineScopeの拡張関数になっているという答えにたどり着けた場合、自分が作る関数もCoroutineScopeを取る拡張関数にすればいいのでは・・・!という答えになるかもしれません。
もしその発想にたどり着けた暁にはもうゴールは目前で、最後の一押しとしてLintがcoroutineScope { }に置き換えるよう教えてくれます。かしこい!!!
// Lint で coroutineScope { } に置き換えるよう教えてくれる
private suspend fun CoroutineScope.downloadFile2() {
launch { } // this が CoroutineScope なので問題は無い
launch { }
}ただ、引数にコルーチンスコープを取る場合は教えてくれないので注意。
private suspend fun downloadFile3(scope: CoroutineScope) {
scope.launch {
}
}ところで、なんでコルーチンを起動するのにコルーチンスコープが必要なんでしょうか?
スレッドや Future 、Promise はどこでも作れるじゃないですか、なんでこんな仕様なの?めんどくせ~~
と思うかもしれませんが、これはスレッドやFuture、Promiseにはない安全設計のため、この様になっています。
この安全設計が構造化された並行性と呼ばれるものです。
Kotlin Coroutinesに構造化された並行性を導入した方の、こちらの記事もどうぞStructured concurrency
Today marks the release of a version 0.26.0 of kotlinx.coroutines library and an introduction of structured concurrency to Kotlin…
https://elizarov.medium.com/structured-concurrency-722d765aa952
goto文と何ら変わらないという話いや、上記の画像はあんまり関係ないのですが、
二人三脚という競技は、例えば解けてしまった場合は結び直して再出発する必要があります。
いきなり相方がどっか走り出したらルール違反になります。
おおむね、プログラミングの世界でも、起動した並列処理がどこか行方不明にならないよう待ち合わせしたり、
時にはエラーになったら他の並列処理を終了に倒したいときがあります。分割ダウンロードを作るとか。
JavaScriptのPromiseでは、Promise.allを使うことで全てのPromiseが終わるまで待つ事ができます。
しかし、特に待ち合わせとか何もしない場合は独立してPromiseが動きます。これはスレッドやFutureにも言えますが。
例えば以下のコード、main5関数の方は明示的に並列処理を待ち合わせしていますが、main6の方は非同期処理を開始するだけ開始してそのままにしています。main6の呼び出し後は面倒を見ていません。別にPromiseに関係なく、コールバックだろうとそのまま待たずに呼び出し元へ戻ったら同じことが言えます。
async function main4() {
// 待ち合わせする
await main5()
// これは待ち合わせしないのですぐ呼び出し元(ここ)に戻って来る
main6()
}
// これは全ての Promise を待ち合わせする。待ち合わせが終わるまで呼び出し元へは戻らない
async function main5() {
const multipleRequest = [
fetch("https://example.com"),
fetch("https://example.com"),
fetch("https://example.com")
]
return await Promise.all(multipleRequest)
}
// これは非同期処理を開始するだけ開始して、呼び出し元にすぐに戻る
function main6() {
fetch("https://example.com")
fetch("https://example.com")
fetch("https://example.com")
}構造化された並行性ではこれを問題視しています。
例えば、非同期処理を待たないので、コードの理解が困難になります。
冒頭で行った通り、非同期処理、コールバックたちはプログラミングのいろはを全て破壊していったので、forは順番を守らない、try-catch-finallyだとcatchに来ない、finallyが非同期処理よりも先に呼ばれるしで、理解が困難になります。
(finallyが先に呼ばれるせいでAutoCloseable#use { }、他の言語のusing { }が使えない等)
それではKotlin Coroutinesを見ていきましょう。賢い仕組みがあります。そして困惑するかもしれません。
以下のコードを見てください。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
runMultipleTask()
}
}
private suspend fun runMultipleTask() {
coroutineScope { // コルーチンスコープを作成
println("coroutineScope 爆誕")
listOf(3_000L, 5_000L, 10_000L).forEach { time -> // 3・5・10 秒待つコルーチンを並列で起動
launch {
delay(time)
println("$time ミリ秒待ったよ!")
}
}
}
println("おしまい!")
}
}上でいった通り、suspend funの中ではlaunch { }出来ないため、coroutineScope { }を使いました。
これの実行結果ですが、予想できますか?
coroutineScope 爆誕
3000 ミリ秒待ったよ!
5000 ミリ秒待ったよ!
10000 ミリ秒待ったよ!
おしまい!先に おしまい! が来るのかと思いきや、coroutineScope { }で起動した3・5・10 秒待つコルーチンを並列で起動を全て待ってからおしまいに進んでいます。
最後に10000 ミリ秒待ったよ!が出ると思ったそこのキミ。多分スレッドやFuture、Promiseから来ましたね?
これが構造化された並行性と呼ばれるもので、並列で起動した子コルーチンが全て終わるまで、親のコルーチンが終わらないという特徴があります。
明示的に待つ必要はなく、暗黙のうちに全ての子の終了を待つようになっています。(もちろん明示的に待つ事もできます。join())
この子が終わっているかの追跡に、コルーチンスコープを使っているんですね。新しいコルーチンの起動にコルーチンスコープが必要なのもなんとなく分かる気がする。
スレッドやFuture、Promiseの場合、この構造化された並行性が無いため、
非同期処理を開始するだけ開始して、成功したかまでは確認しない。すぐ戻って来るよろしく無い関数が作れてしまいます。
一方Kotlin Coroutinesでは基本的に並列処理が終わる前に戻ってくるような関数は作れません。
(コルーチンスコープの使い方を間違えていなければ)
こっちのが分かりやすいかな。Kotlin Coroutinesでは、並列実行した処理でどれか1つが失敗したら他も失敗するのがデフォルトです。生き残っている並列処理をそのまま続行したいことのほうが稀なはずなので、これは嬉しいはず。
どれか1つのPromise / Future / スレッドで失敗したら他も失敗にするという処理、なかなか面倒な気がします。
また、キャンセルさせたいと思っても全てのPromise / Future / スレッドに失敗を伝搬する何かを自前で実装する必要があります。
Kotlin Coroutinesのデフォルトでは、子が失敗した場合その他の子も全てを失敗にします。
どれか1つが失敗したら、親が検知して、子供に失敗、もといキャンセルを命令します。ちなみにキャンセルは例外の仕組み動いているのですが、後述します。
以下のコードで試してみましょう。
まだ習っていないCancellationExceptionとかが出てきますがすいません。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
try {
splitTask()
} catch (e: RuntimeException) {
println("errorTask() が投げた RuntimeException をキャッチ!")
}
}
}
// delayTask を3つ、errorTask を1つ並列で実行する
private suspend fun splitTask() = coroutineScope {
launch { delayTask(1) }
launch { delayTask(2) }
launch { delayTask(3) }
launch { errorTask() }
}
private suspend fun errorTask() {
delay(3_000)
throw RuntimeException()
}
private suspend fun delayTask(id: Int) {
try {
delay(10_000)
} catch (e: CancellationException) {
println("失敗 $id")
throw e
}
}
}logcatがこうです。
失敗 1
失敗 2
失敗 3
errorTask() が投げた RuntimeException をキャッチ!delayTaskで10秒待っている間に、errorTask()が例外を投げました。
すると、エラーを伝搬するため、他のdelayTask()へCancellationException例外が投げられます。
子を失敗させたら、最後に呼び出し元へエラーを伝搬させます。呼び出し元のcatchで例外をキャッチできるようになります。
CancellationExceptionの話はまだしてないのであれですが、キャンセルが要求されるとこの例外がスローされます。
ルールとしてCancellationExceptionはcatchしたら再 throwする必要があるのですが、これも後述します。
呼び出し元はRuntimeExceptionをキャッチしておけば例外で落ちることはないですね。
もちろん、すべての子の待ち合わせしつつ、子へキャンセルを伝搬させない方法ももちろんあります。
並列で処理するけど、他の並列処理に依存していない場合に使えると思います。
コルーチンスコープを頼りにしているので、コルーチンスコープを適切に使わない場合は普通に破綻します。思わぬエラーです。
例えば以下はすべての子を待ち合わせしません。スコープが違うので他人の子です。我が子以外には興味がないサスペンド関数さん。
private suspend fun runMultipleTask() {
coroutineScope {
println("coroutineScope 爆誕")
listOf(3_000L, 5_000L, 10_000L).forEach { time ->
lifecycleScope.launch { // coroutineScope { } の scope 以外を使うと破綻する
delay(time)
println("$time ミリ秒待ったよ!")
}
}
}
println("おしまい!")
}coroutineScope 爆誕
おしまい!
3000 ミリ秒待ったよ!
5000 ミリ秒待ったよ!
10000 ミリ秒待ったよ!これだって、以下のように書くとエラーが伝搬しません。それから呼び出し元で例外をキャッチできないので絶対やめましょう。
我が子以外には説教をしないサスペンド関数です。
lifecycleScope.launch(Dispatchers.Default) {
try {
splitTask()
} catch (e: RuntimeException) {
// errorTask() が coroutineScope { } の scope じゃないのでここではキャッチできません。エラーが伝搬しないので。
println("errorTask() が投げた RuntimeException をキャッチ!")
}
}
// 省略...
private suspend fun splitTask() = coroutineScope {
launch { delayTask(1) }
launch { delayTask(2) }
launch { delayTask(3) }
lifecycleScope.launch { errorTask() } // coroutineScope { } の scope を使っていない。これだと伝搬しない。
}キャッチしきれなくて普通にクラッシュします。
Process: io.github.takusan23.kotlincoroutienspractice, PID: 29855
java.lang.RuntimeException
at io.github.takusan23.kotlincoroutienspractice.MainActivity.errorTask(MainActivity.kt:65)
at io.github.takusan23.kotlincoroutienspractice.MainActivity.access$errorTask(MainActivity.kt:28)
at io.github.takusan23.kotlincoroutienspractice.MainActivity$errorTask$1.invokeSuspend(Unknown Source:14)以上!!!!
構造化された並行性!!多分クソわかりにくかったと思う。
コルーチンのドキュメント通りに歩くと、次はこれです。
Cancellation and timeouts
これも大事で、これを守らないとKotlin Coroutinesの売り文句とは裏腹に、思った通りには動かないコードが出来てしまいます。
というわけでキャンセル、読んでいきましょう。
いくつかありますがlaunch { }したときの返り値のJobを使う場合。Job#cancelが生えているので呼べばキャンセルできます。
例えば以下のJetpack Composeで出来たカウントアップするやつでは、
開始ボタンを押したらループがコルーチンで開始、終了ボタンを押したらコルーチンをキャンセルさせて、カウントアップを止めます。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent { MainScreen() }
}
}
@Composable
private fun MainScreen() {
val scope = rememberCoroutineScope()
var currentJob = remember<Job?> { null }
val count = remember { mutableIntStateOf(0) }
Scaffold { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Text(
text = count.intValue.toString(),
fontSize = 24.sp
)
Button(onClick = {
// launch の返り値を持つ。キャンセルに使う
currentJob = scope.launch {
while (isActive) { // コルーチンがキャンセルされると isActive が false になる
delay(1_000) // 1 秒一時停止する
count.intValue++
}
}
}) {
Text(text = "カウントアップ開始")
}
Button(onClick = {
// キャンセルする
currentJob?.cancel()
}) {
Text(text = "カウントアップ停止")
}
}
}
}他にもCoroutineScope#cancelやCoroutineContext#cancelChildrenを使って、子のコルーチンを全てキャンセルさせる事もできます。cancel()だとこれ以降コルーチンを作ることが出来ないので、それが困る場合はcancelChildren()を使うといいと思います。
ただ、Androidだとコルーチンスコープのキャンセルはあんまりしないと思います。
というのも、既にAndroidが用意しているコルーチンスコープ、lifecycleScope / viewModelScope / rememberCoroutineScope() / LaunchedEffectたちは、それぞれのライフサイクルに合わせて自動でキャンセルする機能を持っています。
lifecycleScopeではonDestroy(確か)、rememberCoroutineScope() / LaunchedEffectではコンポーズ関数が表示されている間。
なので、例えば以下のコードでは、条件分岐でコンポーズ関数が表示されなくなったらLaunchedEffectを勝手にキャンセルしてくれます。コンポーズ関数が画面から消えたのにカウントアップだけ残り続けるなんてことは起きません。
@Composable
private fun MainScreen() {
val isEnable = remember { mutableStateOf(false) }
if (isEnable.value) {
// true の間のみ、false になった場合は LaunchedEffect が呼ばれなくなるのでキャンセルです
LaunchedEffect(key1 = Unit) {
while (isActive) {
delay(1_000)
println("launched effect loop ...")
}
}
}
Button(onClick = { isEnable.value = !isEnable.value }) {
Text(text = "isEnable = ${isEnable.value}")
}
}cancel()したあとにjoin()することで終わったことを確認できます。別にキャンセルしなくても終わるまで待ちたければjoin()すれば良いです。cancel()はあくまでもキャンセルを命令するだけで、キャンセルの完了を待つ場合はjoin()が必要です。
後述しますが、キャンセルをハンドリングして後始末をする事ができるため、その後始末を待つ場合はjoin()が役立つかも。
また、cancelAndJoin()とかいう、名前通り2つを合体させた関数があります。
まずはキャンセルの仕組みをば。
キャンセルの仕組みですが、キャンセル用の例外CancellationExceptionをスローすることで実現されています。
キャンセルが要求された場合は上記の例外をスローするわけですが、誰がスローするのかと言うと、一時停止中のサスペンド関数ですね。以下の例だとdelay()さんです。
while (isActive) { // isActive はキャンセルしたら false になるよ
delay(1_000) // キャンセルが要求されたらキャンセル例外を投げるよ!
println("loop ...")
}delay()がキーパーソンになります。delay()のドキュメントを確認しますが、指定時間待っている間にコルーチンがキャンセルされた場合は関数自身がキャンセル例外をスローすると書いています。delay()はキャンセルに協力的に作られているので良いのですが、自分でサスペンド関数を書いた場合はちゃんとキャンセルに協力的になる必要があります。
この話を次でします。
サスペンド関数を try-catch / runCatching で囲った場合、注意点があります。CancellationException例外をスローすることでキャンセルが実現しているわけですが、try-catchやrunCatchingでCancellationExceptionをキャッチした場合はどうなるでしょうか?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
// 5秒になるまで1秒ごとに経過を logcat に出す
val timer = launch { printTimer() }
// キャンセルしてみる
delay(2_000)
timer.cancel()
println("キャンセルしたよ")
// コルーチンが終わるのを待つ
timer.join()
println("おわったよ")
}
}
private suspend fun printTimer() {
runCatching { delay(1_000) }
println("1秒経過")
runCatching { delay(1_000) }
println("2秒経過")
runCatching { delay(1_000) }
println("3秒経過")
runCatching { delay(1_000) }
println("4秒経過")
runCatching { delay(1_000) }
println("5秒経過")
}
}結果はこれです。
キャンセル後もキャンセルされずに処理が続行されています。これはキャンセル例外をキャッチしてしまったのが理由です。
1秒経過
キャンセルしたよ
2秒経過
3秒経過
4秒経過
5秒経過
おわったよ修正方法としては、
try-catchでは最低限の例外だけをキャッチするCancellationExceptionをキャッチしたら再スローするのどれかが必要です。まずはtry-catchから。
// 最低限だけキャッチする
private suspend fun printTimer() {
try {
delay(1_000)
} catch (e: RuntimeException) {
// 必要な例外だけキャッチする。ちなみに delay は RuntimeException スローしないと思いますが
}
println("1秒経過")
try {
delay(1_000)
} catch (e: RuntimeException) {
// ...
}
println("2秒経過")
// 以下省略...
}もしくは、Exception を網羅的にキャッチするが、CancellationException だけは再スローする。
private suspend fun printTimer() {
try {
delay(1_000)
} catch (e: CancellationException) {
// キャンセル例外だけはキャッチして再 throw
throw e
} catch (e: Exception) {
// Exception を網羅的にキャッチ
}
println("1秒経過")
try {
delay(1_000)
} catch (e: CancellationException) {
// キャンセル例外だけはキャッチして再 throw
throw e
} catch (e: Exception) {
// Exception を網羅的にキャッチ
}
println("2秒経過")
// 以下省略...
}runCatching { }にも言えます。Kotlin CoroutinesがcencellableRunCatching { }とか、suspendRunCatching { }を作ってくれないのが悪い気もする。Provide a `runCatching` that does not handle a `CancellationException` but re-throws it instead. · Issue #1814 · Kotlin/kotlinx.coroutines
Problem Description The Kotlin StdLib contains a function named runCatching. It tries the provided lambda and catches any Throwable that is not caught or thrown by the lambda. However, using runCat...
https://github.com/Kotlin/kotlinx.coroutines/issues/1814
runCatching { }のキャンセル対応版を作るかKotlin: runCatching と coroutine - nashcft's blog
内容的には以下の issue で議論されていることの抜粋のようなものだが、つまるところ現状 Kotlin Coroutines と runCatching (より詳細には runCatching の block 内で suspend function を呼んだ場合) の食い合わせが悪い問題に対してどういう対処ができるのかについて、備忘としてまとめておく。 github.com runCatching は全ての例外を catch する 現時点のコード: github.com public inline fun <R> runCatching(block: () -> R): Result<R> …
https://nashcft.hatenablog.com/entry/2023/06/16/094916
// 対策版 runCatching に置き換える
private suspend fun printTimer() {
suspendRunCatching { delay(1_000) }
println("1秒経過")
suspendRunCatching { delay(1_000) }
println("2秒経過")
suspendRunCatching { delay(1_000) }
println("3秒経過")
suspendRunCatching { delay(1_000) }
println("4秒経過")
suspendRunCatching { delay(1_000) }
println("5秒経過")
}
/** コルーチンのキャンセル例外はキャッチしない[runCatching] */
inline fun <T, R> T.suspendRunCatching(block: T.() -> R): Result<R> {
return try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
}もしくは、ResultクラスにgetOrCancel()みたいな拡張機能を作って、Resultを返すけど、もしキャンセル例外で失敗していればスローするとか。
private suspend fun printTimer() {
runCatching { delay(1_000) }.getOrCancel()
println("1秒経過")
runCatching { delay(1_000) }.getOrCancel()
println("2秒経過")
runCatching { delay(1_000) }.getOrCancel()
println("3秒経過")
runCatching { delay(1_000) }.getOrCancel()
println("4秒経過")
runCatching { delay(1_000) }.getOrCancel()
println("5秒経過")
}
/** [Result]の失敗理由がキャンセル例外だった場合は、キャンセル例外をスローする拡張関数 */
fun <T> Result<T>.getOrCancel(): Result<T> = this.onFailure {
if (it is CancellationException) {
throw it
}
}try-catchもrunCatching { }もどっちも厳しい場合は、try-catchやrunCatching { }の後にensureActive()を呼び出す手もあります。ensureActive()はCoroutineScopeの拡張関数なのでコルーチンスコープが必要で、coroutineScope { }で囲ったり、launch { }の中で使わないといけないのが玉に瑕。
private suspend fun printTimer() = coroutineScope {
try {
delay(1_000)
} catch (e: Exception) {
// ...
}
ensureActive() // この時点でキャンセルされている場合、キャンセル例外をスローする
println("1秒経過")
try {
delay(1_000)
} catch (e: Exception) {
// ...
}
ensureActive() // この時点でキャンセルされている場合、キャンセル例外をスローする
println("2秒経過")
// 以下省略...
}private suspend fun printTimer() = coroutineScope {
runCatching { delay(1_000) }
ensureActive() // runCatching { } の後にキャンセルチェック
println("1秒経過")
runCatching { delay(1_000) }
ensureActive() // runCatching { } の後にキャンセルチェック
println("2秒経過")
runCatching { delay(1_000) }
ensureActive() // runCatching { } の後にキャンセルチェック
println("3秒経過")
runCatching { delay(1_000) }
ensureActive() // runCatching { } の後にキャンセルチェック
println("4秒経過")
runCatching { delay(1_000) }
ensureActive() // runCatching { } の後にキャンセルチェック
println("5秒経過")
}出力結果はこうです。
キャンセル後は余計な処理がされていないことを確認できました!
1秒経過
キャンセルしたよ
おわったよさて、ここからが大事です。(ここまでも大事ですが)
さっき書いたカウントアップするだけのコードはキャンセル出来ましたが、これはdelay()やisActiveがキャンセルに協力しているからキャンセルできただけです。
自分でサスペンド関数を書く場合は、キャンセルに協力的になる必要があります。
例えば以下の、コルーチンの中で、OkHttpでGETリクエストを同期的に呼び出すコードを動かしてみます。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// 別スレッドを指定したコルーチン
val internetJob = lifecycleScope.launch(Dispatchers.IO) {
repeatGetRequest()
}
lifecycleScope.launch {
delay(2_000)
println("キャンセルします")
internetJob.cancelAndJoin()
println("cancelAndJoin() 終わりました")
}
}
// OkHttp の同期モードで何回か GET リクエストを投げます
private suspend fun repeatGetRequest() {
// 5 回くらい
repeat(5) {
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
val response = OkHttpClient().newCall(request).execute()
println("レスポンス ${response.code}")
}
}
}読者さんのインターネットの速度によっては、以下のように再現できないかも。なのであれなのですが。(開発者向けオプションのネットワーク速度を変更する、使わなければ128kのpovo 2.0を契約する等)
しかし、さっきのカウントアップのときとは違い、cancel()を呼んだのにもかかわらず、GETリクエストが続行されています。
これはキャンセルに協力的ではありませんね。 キャンセルしたらインターネット通信を始めないでほしいです。ギガが減るんでね。
レスポンス 200
レスポンス 200
キャンセルします
レスポンス 200
レスポンス 200
レスポンス 200
cancelAndJoin() 終わりましたギガが減るのも良くないけど、キャンセルが適切に行われないとクラッシュを巻き起こす可能性もあります。
画面回転や、Fragment の破棄後に非同期処理が終わり、破棄されているのにUI更新しようとして落ちるパターン。ViewModelが来る前まではみんな引っかかってたはず。getActivity() != nullとか、Fragment#isAdded() == trueとかで分岐してなんとかしのいでた。
例に漏れずコルーチンでも、UI破棄のタイミングでキャンセルを要求したのは良いものの、キャンセル対応のサスペンド関数を書いていないと、破棄後にUI更新する羽目になりやっぱり同じエラーに鳴ってしまいます。
まあUI関係ないならViewModelに書けよという話ではあるんですが。
いくつか、キャンセルに関連する関数、フラグがあります。
yield()CoroutineScope#isActiveCoroutineScope#ensureActive()try-catch / runCatchingのときにも触れましたが、isActiveとensureActive()はコルーチンスコープの拡張関数になっていて、
サスペンド関数の中をcoroutineScope { }で囲ってあげるか、launch { }の直下で使う必要があります。
まあyield()もよく見るとcoroutineScope { }を使っているので適当にcoroutineScope { }で囲っとけばいいんじゃない(適当)
今回なら、インターネット通信を始める前にキャンセルされているか確認すれば良さそうですね。
直前にensureActive()を呼ぶか、isActiveで確認を入れれば良さそうですね。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// 別スレッドを指定したコルーチン
val internetJob = lifecycleScope.launch(Dispatchers.IO) {
repeatGetRequest()
}
lifecycleScope.launch {
delay(2_000)
println("キャンセルします")
internetJob.cancelAndJoin()
println("cancelAndJoin() 終わりました")
}
}
// OkHttp の同期モードで何回か GET リクエストを投げます
private suspend fun repeatGetRequest() = coroutineScope {
// 5 回くらい
repeat(5) {
// キャンセル済みならリクエストしない
ensureActive()
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
val response = OkHttpClient().newCall(request).execute()
println("レスポンス ${response.code}")
}
}
}こんな感じにキャンセル後もリクエストが継続されているような挙動じゃなくなりました。
自分で作ったサスペンド関数がキャンセル可能になりました!!!キャンセル後も、キャンセルする前のリクエストが残ってるせいで微妙に分かりにくい。
レスポンス 200
レスポンス 200
キャンセルします
レスポンス 200
cancelAndJoin() 終わりましたもしwhileループをしている場合も同様で、isActiveでループを抜けられるようにするか、ensureActive()で例外を投げる必要があります。
// OkHttp の同期モードで何回か GET リクエストを投げます
private suspend fun repeatGetRequest() = coroutineScope {
// 5 回くらい
var count = 0
while (count < 5 && isActive) { // isActive も確認する
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
val response = OkHttpClient().newCall(request).execute()
println("レスポンス ${response.code}")
count++
}
}キャンセルされているか確認しろと言われても、1行毎に入れていったら洒落にならないでしょう。
CPUを大量に消費する処理(フィボナッチ数列を計算する)とかの、
Android でのコルーチンに関するベスト プラクティス | Kotlin | Android Developers
https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ja
それから、delay()やそのほかkotlinx.coroutinesパッケージ傘下にあるサスペンド関数(最初から用意されているサスペンド関数)は、基本的にキャンセルに対応しているため、ensureActive()とかで確認せずともキャンセルされたら例外を投げてくれるはずです。
逆を言えば最初から用意されていない、自分でサスペンド関数を書く場合はキャンセル出来るよう心がける必要があります。
delaywithContext(後述)
join / cancelAndJoin確かに2パターンあります。
微々たる差ではあるのですが、isActiveの方は例外を投げないので、while { }の下でなにか処理をしたい場合にちょっと楽かもしれません。
ただ、ensureActive()でもtry-finallyであとしまつ出来るので、どっちでもいい気がします。
private suspend fun exampleEnsureActive() = coroutineScope {
try {
while (true) {
ensureActive()
// 重い処理
}
} finally {
// あとしまつとか
}
}
private suspend fun exampleIsActive() = coroutineScope {
while (isActive) {
// 重い処理
}
// あとしまつとか
}そういえば、ensureActive()と違い、isActiveの方は例外を投げないから、isActiveでキャンセルを実装したらキャンセル後も処理が続行してしまうのでは、、、と思う方がいるかも知れません。例外の場合は投げれば後続の処理は実行されませんからね。
coroutineScope {
ensureActive() // コルーチンキャンセル時は例外を投げる
println("生きてる") // 例外投げたらここに進まない
}coroutineScope {
if (isActive) {
println("生きてる") // コルーチン生きてる時
}
println("あれ?") // ここコルーチンキャンセル時も来ちゃうんじゃない?
}たしかにそれはそうです。ただ、これはtry-finallyすればensureActive()でも出来るやつなのでそういうものだと思います。
isActiveを使った実装方法だと自作サスペンド関数がキャンセルに対応できないのかというと、そんなことはなくて、今回使ったcoroutineScope { }がキャンセルに対応しています。coroutineScope { }のブロック内(波括弧の中身)が終了して、呼び出し元へ戻る際にキャンセルチェックが入ります。
戻った際にキャンセルが要求されていることが分かったらcoroutineScope { }自身が例外を投げます。
よって、今回書いてきたコードではどちらを使ってもcoroutineScope { }かensureActive()が例外を投げてくれるので、キャンセルに対応することになります。
なんて読むのか調べたらいーるどって読むらしい。
記事書き終わった後に良い例を思い出したので書いてみる。
ドキュメントではスレッドを譲るって書いてあるけど、なんか難しくて避けてた。
これはスレッドを専有するような処理を書く時に使うと良いみたい。
まだ習ってない物を使いますが、limitedParallelism()を使い、1スレッドで処理されるコルーチンを2つ起動します。1スレッドしかないため、どちらかのコルーチンがスレッドを専有、ずっと終わらない処理を書いた場合はもう片方のコルーチンは処理されないことになります。例を書きます。
class MainActivity : ComponentActivity() {
/** とりあえずは 1スレッド で処理されるコルーチンを作るためのものだと思って */
private val singleThreadDispatcher = Dispatchers.Default.limitedParallelism(1)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// シングルスレッドでコルーチンを2つ起動
lifecycleScope.launch(singleThreadDispatcher) {
while (isActive) {
// スレッドを専有する。
// ここでは Thread.sleep() を使いスレッドを譲らずに経過時間までブロックする
// 本来は Thread.sleep() は使わない。スレッドをブロックする例のため意図的に使っている。
// ブロックするならインターネット通信等の IO 処理でもいいよ
Thread.sleep(3_000)
println("[launch 1] Thread.sleep")
}
}
lifecycleScope.launch(singleThreadDispatcher) {
// 同じものを作る
while (isActive) {
Thread.sleep(3_000)
println("[launch 2] Thread.sleep")
}
}
}
}これでlogcatを見てみると、[launch 1]しかログが出ていません。
なぜならシングルスレッドしか無い上に、スレッドをブロックする無限ループを書いて専有してしまっているためです。
[launch 1] Thread.sleep
[launch 1] Thread.sleep
[launch 1] Thread.sleep
[launch 1] Thread.sleep
[launch 1] Thread.sleepここでyield()が役に立ちます。説明どおりならスレッドを譲ってくれるはずです。
ループ毎にyield()を呼び出してみましょう。
class MainActivity : ComponentActivity() {
/** とりあえずは 1スレッド で処理されるコルーチンを作るためのものだと思って */
private val singleThreadDispatcher = Dispatchers.Default.limitedParallelism(1)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// シングルスレッドでコルーチンを2つ起動
lifecycleScope.launch(singleThreadDispatcher) {
while (isActive) {
yield() // 他にコルーチンあれば譲る
// スレッドを専有する。
// ここでは Thread.sleep() を使いスレッドを譲らずに経過時間までブロックする
// 本来は Thread.sleep() は使わない。スレッドをブロックする例のため意図的に使っている。
// ブロックするならインターネット通信等の IO 処理でもいいよ
Thread.sleep(3_000)
println("[launch 1] Thread.sleep")
}
}
lifecycleScope.launch(singleThreadDispatcher) {
// 同じものを作る
while (isActive) {
yield() // 他にコルーチンあれば譲る
Thread.sleep(3_000)
println("[launch 2] Thread.sleep")
}
}
}
}これで、logcatを見てみると、[launch 2]の出力がされるようになりました。
スレッドを専有するような処理ではyield()を入れておくと良いかもですね!
キャンセルできるサスペンド関数は、キャンセル時はキャンセル例外を投げるため、try-finallyが完全に動作します。
処理が完了するか、はたまたキャンセルされるかわかりませんが、finallyに書いておけばどちらにも対応できます。プログラミングのいろはがようやく戻ってきました。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
// 5秒で終わってみる
val job1 = launch { tryFinally() }
delay(5_000)
job1.cancelAndJoin()
println("---")
// 10秒で終わってみる
val job2 = launch { tryFinally() }
delay(10_000)
job2.cancelAndJoin()
}
}
private suspend fun tryFinally() {
try {
delay(10_000)
println("10秒待った")
} finally {
println("finally ですよ")
}
}
}こんな感じにキャンセルされる、されないに関係なくfinallyが実行できています。やったやった!
finally ですよ
---
10秒待った
finally ですよコルーチンがキャンセルした後、新しくコルーチンを起動することが出来ません。
あんまりないかもしれませんが、どうしてもfinallyでサスペンド関数を呼び出したいときの話です。
キャンセル済みの場合、finally { }の中ではコルーチンを起動しても、キャンセル済みなのでensureActive()を呼び出すと例外をスローするし、isActiveはfalseになります。
サスペンド関数は動かなくなります。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
val job = launch { tryFinally() }
delay(5_000)
println("キャンセル")
job.cancelAndJoin()
}
}
private suspend fun tryFinally() {
try {
delay(10_000)
println("10秒待った")
} finally {
sendLog()
println("ログ送信")
}
}
private suspend fun sendLog() = coroutineScope {
// キャンセルチェック
ensureActive()
// TODO ログ送信
}
}どうしてもfinally { }でサスペンド関数を呼び出したい場合は、withContext(NonCancellable) { }で処理をくくると、サスペンド関数も一応動くようになります。withContextは後述します。が、NonCancellableを引数に渡すと、キャンセル不可の処理を実行できるようになります。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
val job = launch { tryFinally() }
delay(5_000)
println("キャンセル")
job.cancelAndJoin()
}
}
private suspend fun tryFinally() {
try {
delay(10_000)
println("10秒待った")
} finally {
// キャンセル不可にする
withContext(NonCancellable) {
sendLog()
println("ログ送信")
}
}
}
private suspend fun sendLog() = coroutineScope {
// キャンセルチェック
ensureActive()
println("isActive = $isActive")
// TODO ログ送信
}
}出力結果はこうです、ensureActive()がキャンセル例外をスローしなくなりました。
一方、isActiveとかも、NonCancellableが付いている場合はtrueになるので注意です。キャンセルされたかの判定が壊れます。
あくまでリソース開放とかの最小限にとどめましょうね。
キャンセル
isActive = true
ログ送信NonCancellable、よく見るとlaunch { }にも渡せるんですが、withContext { }以外で使ってはいけません。タイムアウトもできます。withTimeout { }を使うと指定時間以内にコルーチンが終了しなかった場合に、withTimeout { }の中の処理はキャンセルし、また関数自身もTimeoutCancellationExceptionをスローします。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
try {
withTimeout(5_000) {
tryFinally()
}
} catch (e: TimeoutCancellationException) {
println("たいむあうと!")
throw e
}
}
}
private suspend fun tryFinally() {
try {
delay(10_000)
println("10秒待った")
} finally {
println("finally")
}
}
}finally
たいむあうと!先述の通り、タイムアウトしたらwithTimeoutは例外を投げるので、投げられた場合後続する処理が動きません。
lifecycleScope.launch {
withTimeout(5_000) {
tryFinally()
}
println("タイムアウト例外が出たら動かない") // withTimeout が例外を投げるせいでここには来ない
}それが困る場合は、代わりにnullを返すwithTimeoutOrNull { }を使うと良いです。
lifecycleScope.launch {
val resultOrNull = withTimeoutOrNull(5_000) {
tryFinally()
}
println("タイムアウト例外が出たら動かない")
}finally
タイムアウト例外が出たら動かない
The Silent Killer That’s Crashing Your Coroutines
There’s only one safe way to deal with cancellation exceptions in Kotlin, and it’s not to re-throw them
https://medium.com/better-programming/the-silent-killer-thats-crashing-your-coroutines-9171d1e8f79b
回答: なりません
ensureActive()の説明では以下のコードと大体同じことをやっていると書いています。if (!isActive) {
throw CancellationException()
}それでは自分で投げてみましょう!キャンセル時の挙動は、先述の通り、
isActiveがfalseであるはずで、ensureActive()も例外を投げるはずで、withContext { }も使えないはず、なので、それも出力して見てみることにします。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
try {
throwCancel()
} catch (e: CancellationException) {
println("CancellationException !!!")
println("isActive = $isActive")
// ensureActive() でも例外を投げるか確認
try {
ensureActive()
println("ensureActive() スローせず")
} catch (e: CancellationException) {
println("ensureActive() キャッチ")
throw e
}
throw e
}
}
}
private suspend fun throwCancel() {
delay(3_000)
throw CancellationException()
}
}で、logcatに出てきたのがこれです。
CancellationException !!!
isActive = true
ensureActive() スローせず
withContext に入りましたどうやらダメみたいですね。
キャンセル例外を投げるだけではキャンセル扱いにはならないみたいです。ちゃんと正規ルートでキャンセルしましょう。
これはほとんど無いと思いたい。。。!
ただ、CancellationExceptionがKotlin Coroutinesで追加された例外ではなく、Javaのエイリアスになっているので,Kotlin Coroutinesのことを一切考慮していないJava / Kotlinコードからその例外を投げられる可能性は、、、可能性だけならありますね。
つぎはこれ、サスペンド関数の話です。ついに並列処理の話ができます。
今まで通り、そのまま書けば直列処理です。
普通に書くだけで順番通りに処理されるとか、コールバックのときにはあり得なかったことですね。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
request()
}
}
private suspend fun request() {
// 直列で実行する
val time = measureTimeMillis {
requestInternet()
requestInternet()
}
println("$time ms で完了")
}
private suspend fun requestInternet() = coroutineScope {
ensureActive()
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
OkHttpClient().newCall(request).execute()
}
}結果はこう。直列処理なので、1つ目が終わるまで2つ目は呼ばれません。
1474 ms で完了次に並列処理です。1番目の処理を待たずに並列で走らせることが出来ます。async { }とawait()を使います。
async { }の使い方はlaunch { }のそれと同じなのですが、launch { }と違って返り値を返せます!await()で返り値を取得できます。
それ以外は大体同じなのでコルーチンスコープが必要なのも同様です。
private suspend fun request() = coroutineScope { // scope を用意
// 並列で実行する
val time = measureTimeMillis {
// async { } はすぐ実行されます
val request1 = async { requestInternet() }
val request2 = async { requestInternet() }
// なんかやる...
// 待ち合わせする
val response1 = request1.await()
val response2 = request2.await()
}
println("$time ms で完了")
}直列のときよりも大体半分の時間で終わっています。
731 ms で完了数が多い場合は配列に入れてawaitAll()すると良いかもしれません。
見た目が良くてすき
private suspend fun request1() = coroutineScope {
// 並列で実行する
val time = measureTimeMillis {
// listOf でも
val responseList = listOf(
async { requestInternet() },
async { requestInternet() }
).awaitAll()
val (response1, response2) = responseList
}
println("$time ms で完了")
}
private suspend fun request2() = coroutineScope {
// 並列で実行する
val time = measureTimeMillis {
// map で async を返して awaitAll() する
val responseList = (0 until 2) // [0, 1]
.map { async { requestInternet() } } // すべて並列で実行
.awaitAll() // 全て待つ
// 取り出す
val (response1, response2) = responseList
}
println("$time ms で完了")
}async { }は何もしない場合はすぐに実行されますが、明示的に開始するように修正することが出来ます。async { }の引数にstart = CoroutineStart.LAZYをつけると、start()を呼び出すまで動かなくなります。
private suspend fun requestInternet() = coroutineScope {
println("requestInternet()")
ensureActive()
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
OkHttpClient().newCall(request).execute()
}
private suspend fun request() = coroutineScope { // scope を用意
// 直列で実行する
val time = measureTimeMillis {
val request1 = async(start = CoroutineStart.LAZY) { requestInternet() }
val request2 = async(start = CoroutineStart.LAZY) { requestInternet() }
// なんかやる...
println("起動前")
// async 開始する
request1.start()
request2.start()
println("開始した")
// 待ち合わせする
val response1 = request1.await()
val response2 = request2.await()
}
println("$time ms で完了")
}出力はこうです。
ちゃんとstart()した後にrequestInternet()が呼ばれてそうですね。
起動前
開始した
requestInternet()
requestInternet()
659 ms で完了で、上記では触れなかったasync { }の使い方をば。
他の言語から来た場合、asyncキーワードは関数宣言時に使うので、このように書きたくなるかなと思います。
// ダメなパターン
@OptIn(DelicateCoroutinesApi::class)
private fun requestInternet1() = GlobalScope.async(Dispatchers.IO) { // 関数宣言時に async を使いたくなる
ensureActive()
val request = Request.Builder().apply {
url("https://example.com")
get()
}.build()
return@async OkHttpClient().newCall(request).execute()
}しかし、構造化された並行性があるKotlin Coroutinesではasync { }は呼び出し側で使うのが良いです。
関数の返り値にasync { }を使う、ではなくサスペンド関数を作ってasync { }の中で呼び出すのが良いです。
というのも、これだと子コルーチンのどれかが失敗した際に、他の子コルーチンをキャンセルする動作が動かないんですよね。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
val job = launch {
try {
task()
} catch (e: RuntimeException) {
// ...
}
}
println("launch した")
delay(3_000)
job.cancel()
println("キャンセル")
job.join()
println("おわった")
}
}
private suspend fun task() = coroutineScope {
listOf(
launch { suspendDelay(tag = "launch") },
async { suspendDelay(tag = "async") },
asyncDelay(),
launch {
// 子コルーチンの1つを失敗にする。全部にキャンセルが行くはず
delay(3_000)
println("例外を投げます")
throw RuntimeException()
}
).joinAll()
}
// ダメなパターン
private fun asyncDelay() = GlobalScope.async {
suspendDelay(tag = "asyncDelay")
}
/** 適当な時間 delay する */
private suspend fun suspendDelay(tag: String) {
try {
delay(10_000)
println("[$tag] 10秒たった")
} catch (e: CancellationException) {
println("[$tag] キャンセル!")
throw e
} finally {
println("[$tag] おわり")
}
}
}出力はこうです
launch した
例外を投げます
[launch] キャンセル!
[launch] おわり
[async] キャンセル!
[async] おわり
キャンセル
おわった
[asyncDelay] 10秒たった
[asyncDelay] おわり構造化された並行性では、子のどれか1つが失敗したら他の子コルーチンにもキャンセルが伝搬するのですが、結果はこうです。launch { suspendDelay(tag = "launch") }、async { suspendDelay(tag = "async") }はちゃんとキャンセルされているのですが、asyncDelay()だけは生き残っています。
これはなぜかと言うと、asyncDelay()だけはコルーチンスコープが違うため、キャンセル命令が行き届いてないのです。launch { }とasync { }はsuspendCoroutine { }のコルーチンスコープを使っていますが、asyncDelay()はGlobalScopeのコルーチンスコープを使っています。
そのため、修正するとしたら、
コルーチンスコープをasyncDelay()の引数に渡すよりは、asyncDelay()関数をサスペンド関数にし、async { }を呼び出す責務を呼び出し側に移動させるのが正解です。async { suspendDelay(tag = "async") }の使い方が正解ですね。
他の言語にあるPromiseとかは、返り値を返すこのスタイルが使われているので注意です。
async { }だと値が返せるんだし、全部async { }でいいのではと。
値を返さない場合はlaunch { }、値を返す必要があればasync { }でいいと思います。launch { }のほうが考えることが少なくてよいです。
というのも、詳しくは例外の章で話すのですが、例外を投げる部分が違ってきます。ほんと微々たる違いなのですが、例外なのでシビアにいきましょう。launch { }は、親のコルーチンまで例外を伝達させます。親まで伝達するため他の子コルーチンもキャンセルになります。
ので、例外をキャッチするにはcoroutineScope { }(親のスコープ)でtry-catchする必要があります。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
try {
coroutineScope {
listOf(
launch { delay(5_000) },
launch { delay(5_000) },
launch {
delay(3_000)
throw RuntimeException() // 3 秒後に失敗
}
)
}
} catch (e: RuntimeException) {
println("投げた RuntimeException をキャッチ")
}
}
}
}async { }は例外を返せます。launch { }と違い、async { }はawait()を使い値を返せるといいましたが、await()で例外も受け取ることが出来ます。
ついでに親のコルーチンスコープまで例外を伝達させます。 親まで伝達するため他の子コルーチンもキャンセルになります。
なので、await()の部分ではなく、try-catchはcoroutineScope { }や親のコルーチンでやる必要があります。ちなみにawait()でtry-catchしてもキャッチできます。ただ親にも伝達します。
逆に期待通り、await()で例外をキャッチできるようにする方法もあります。親に伝搬しない方法。SupervisorJob()やsupervisorScope { }を使うことでawait()で例外をキャッチして、かつ親にも伝搬しないようになります。が、後述します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
try {
coroutineScope { // 親にも例外が行く
val asyncTasks = async {
delay(3_000)
throw RuntimeException() // 3 秒後に失敗
}
try {
asyncTasks.await()
} catch (e: RuntimeException) {
println("await() で RuntimeException をキャッチ")
}
}
} catch (e: RuntimeException) {
println("coroutineScope() で RuntimeException をキャッチ")
}
}
}
}出力がこう
await() で RuntimeException をキャッチ
coroutineScope() で RuntimeException をキャッチlaunch { }やasync { }には引数を渡すことが出来ます。この引数のことをCoroutineContext(コルーチンコンテキスト)といいます。
コルーチンコンテキストには後述するDispatchersなどを設定できるのですが、この章ではその話です。
コルーチンは大量に作れますが、結局はどれかのスレッドで処理されるといいました。launch { }とasync { }では、引数にDispatchersを指定することが出来ます。このDispatchersが実際に処理されるスレッドを指定するものです。
よく使うのが以下の3つです。
| 解説 | スレッド数 | |
|---|---|---|
Dispatchers.Default | メインスレッド以外のスレッドで、CPU を消費する処理向けです。 | 2 個以上CPU のコア数以下 |
Dispatchers.IO | メインスレッド以外のスレッドで、インターネット通信や、ファイル読み書き向けです。 | 少なくとも 64 個、足りなければ増える。 |
Dispatchers.Main | メインスレッドです。 | 1 個 |
DefaultとIOは、Dispatchersが複数のスレッドを持っている(雇っている)形になります。
その時空いているスレッドが使われる感じです。
Dispatchers.Mainに関して、Kotlinのドキュメントには出てこないので不思議に思ったかもしれません。なぜかというとAndroid用(というかGUI向け)に拡張して作られたからです実際に指定してみます。以下のコードを試してみましょう。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
printThread()
}
}
private suspend fun printThread() = coroutineScope {
launch {
println("launch 無指定 ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
println("launch Unconfined ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
println("launch Default ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
println("launch IO ${Thread.currentThread().name}")
}
launch(Dispatchers.Main) {
println("launch Main ${Thread.currentThread().name}")
}
}
}出力がこうです。
出力される順番が前後するかもしれませんが気にせず。
launch Default DefaultDispatcher-worker-2
launch IO DefaultDispatcher-worker-1
launch 無指定 main
launch Unconfined main
launch Main main無指定の場合はmainになっていますが、これは呼び出し元、親のDispatchersを引き継ぐためです。
しかしprintThread()を呼び出しているlaunch { }でも無指定です。特に親のlaunchでも指定がない場合はコルーチンスコープに設定されているDispatchersが使われます。
ちなみに、Dispatchers.Main以外は、複数のスレッドが処理を担当するため、もしスレッドに依存した処理を書く場合は注意してください。Android開発においてはメインスレッドとそれ以外のスレッドという認識(雑)なので特に問題はないはずです。問題がある場合はnewSingleThreadContextの説明も読んでください。
例えば以下のコード、どのスレッドで処理されるかはKotlin Coroutinesのみが知っています。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) { println("Default ${Thread.currentThread().name}") }
lifecycleScope.launch(Dispatchers.Default) { println("Default ${Thread.currentThread().name}") }
lifecycleScope.launch(Dispatchers.Default) { println("Default ${Thread.currentThread().name}") }
lifecycleScope.launch(Dispatchers.Default) { println("Default ${Thread.currentThread().name}") }
lifecycleScope.launch(Dispatchers.Default) { println("Default ${Thread.currentThread().name}") }
}
}予測は出来ません。続きを読めば対策方法があります。
Default DefaultDispatcher-worker-1
Default DefaultDispatcher-worker-2
Default DefaultDispatcher-worker-1
Default DefaultDispatcher-worker-2
Default DefaultDispatcher-worker-1AndroidのlifecycleScopeとJetpack ComposeのrememberCoroutineScope()、LaunchedEffectはデフォルトでMainが指定されています。
が、心配になってきたので一応試しましょう。coroutineContext[CoroutineDispatcher]でDispatchersを取り出せるようです。
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalStdlibApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
println("lifecycleScope = ${lifecycleScope.coroutineContext[CoroutineDispatcher]}")
setContent {
val composeScope = rememberCoroutineScope()
println("rememberCoroutineScope() = ${lifecycleScope.coroutineContext[CoroutineDispatcher]}")
LaunchedEffect(key1 = Unit) {
println("LaunchedEffect = ${lifecycleScope.coroutineContext[CoroutineDispatcher]}")
}
}
}
}結果は認識通り、Mainが使われていました。よかった~
lifecycleScope = Dispatchers.Main.immediate
rememberCoroutineScope() = Dispatchers.Main.immediate
LaunchedEffect = Dispatchers.Main.immediatelifecycleScopeやrememberCoroutineScope、LaunchedEffectのコルーチンスコープはDispatchers.Main.immediateだということが判明しました。
しかし、Dispatchers.MainとDispatchers.Main.immediateの違いはなんなのでしょうか?
・・・??
というわけでドキュメントを見てみましたが、いまいちよく分からなかったので、一緒に書いてあるサンプルコードのコメントを元に実際に動かしてみる。
サンプルコードのコメントを見るに、すでにメインスレッドで呼び出されている場合は、即時実行される。とのこと。試します。
まずはDispatchers.Mainで。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
println("0")
lifecycleScope.launch(Dispatchers.Main) {
println("1")
}
lifecycleScope.launch(Dispatchers.Main) {
println("2")
}
lifecycleScope.launch(Dispatchers.Main) {
println("3")
}
println("4")
}
}これは予想通り、launch { }で囲った1/2/3よりも先に4がでますね。
0
4
1
2
3一方、Dispatchers.Main.immediateをつけると・・・?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
println("0")
lifecycleScope.launch(Dispatchers.Main.immediate) {
println("1")
}
lifecycleScope.launch(Dispatchers.Main.immediate) {
println("2")
}
lifecycleScope.launch(Dispatchers.Main.immediate) {
println("3")
}
println("4")
}
}おお!すでにメインスレッドで呼ばれている場合はlaunch { }が後回しにされずに即時実行されていますね。
0
1
2
3
4ごく、極稀に、自分で作ったスレッドでコルーチンを処理させたい時があります。
あんまり、というか本当に無いと思うのですが、唯一あったのがOpenGL ESですね。
話がそれてしまうので手短に話すと、OpenGL ESは自分を認識するのにスレッドを使っています。
メインスレッド以外でUIを操作できないのと同じように、OpenGL ESもOpenGL ESのセットアップ時に使われたスレッドを自分と結びつけます。
そのため、OpenGL ESの描画を行う際は、スレッドを気にする必要があります。OpgnGL ESにはマルチスレッドは多分ありません。
話を戻して、どうしても自分で作ったスレッドでしか処理できない場合があります。
その場合はnewSingleThreadContext()を使うことで、新しく Java のスレッドを作り、その中で処理されるDispatchersを返してくれます。
はい。Java のスレッドを作ることになります。これを多用した場合はKotlin Coroutinesの売り文句の 1 つ、スレッドより軽量を失うことになります。
そのため、シングルトンにしてアプリケーション全体で使い回すか、使わなくなったら破棄する必要があります。
(親切なことにAutoCloseableインターフェースを実装しているので、use { }拡張関数が使えます!)
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// close() するか use { } を使うこと
val newThreadDispatcher = newSingleThreadContext("new thread !!!!!")
newThreadDispatcher.use {
lifecycleScope.launch(newThreadDispatcher) {
println(Thread.currentThread().name)
}
}
}
}出力がこうです。ちゃんと新しく作ったスレッドで処理されていますね。
new thread !!!!!私もこの記事を書くために初めて使う、し、いまいちどこで使えばいいかよくわからないので多分使わない。
これは特別で、呼び出したサスペンド関数がスレッドを切り替えたら、サスペンド関数を抜けた後もそのスレッドを使うというやつです。
よく分からないと思うので例を書くと。
と、その前にMainの例。Mainで起動したコルーチンでDispatchersを切り替える。withContextはまだ習っていないのですが、CoroutineContext(Dispatchers)を切り替える際に使います。後述します。
@OptIn(ExperimentalCoroutinesApi::class,DelicateCoroutinesApi::class)
class MainActivity : ComponentActivity() {
/** 分かりやすいよう特別に Dispatchers を作る */
private val specialDispatcher = newSingleThreadContext("special thread dispatcher !!!")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Main) {
println("1-1 = ${Thread.currentThread().name}")
specialTask()
println("1-2 = ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Main) {
println("2-1 = ${Thread.currentThread().name}")
specialTask()
println("2-2 = ${Thread.currentThread().name}")
}
}
override fun onDestroy() {
super.onDestroy()
specialDispatcher.close()
}
/** 1秒待つだけ。新しく作ったスレッドで */
private suspend fun specialTask() = withContext(specialDispatcher) {
delay(1_000)
}
}出力がこうです。
ちゃんとspecialTask()を呼び出し終わった後はメインスレッドで処理されていますね。
1-1 = main
2-1 = main
1-2 = main
2-2 = mainつぎにDefaultに書き換えてみます。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
println("1-1 = ${Thread.currentThread().name}")
specialTask()
println("1-2 = ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Default) {
println("2-1 = ${Thread.currentThread().name}")
specialTask()
println("2-2 = ${Thread.currentThread().name}")
}
}実行するたびに若干異なる場合がありますが、 手元ではこうでした。specialTask()が終わった後はDispatchers.Defaultのスレッドが使われてます。が、なぜか違うスレッドが使われてますね。これはDefaultはMainと違って複数のスレッドでコルーチンを処理しているためです。
はじめの方で話した通り、サスペンド関数を抜けた後、(メインスレッドのようなスレッドが 1 つしかない場合を除いて)違うスレッドが使われる可能性があるといいました。その影響です。DefaultDispatcherはDispatchers.Defaultが持っているスレッドなので(要検証)、スレッドこそ違うものが割り当てられましたが、Dispatchersは元に戻ってきていますね。
1-1 = DefaultDispatcher-worker-2
2-1 = DefaultDispatcher-worker-1
2-2 = DefaultDispatcher-worker-2
1-2 = DefaultDispatcher-worker-3最後にUnconfinedです。よく見ててください。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Unconfined) {
println("1-1 = ${Thread.currentThread().name}")
specialTask()
println("1-2 = ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Unconfined) {
println("2-1 = ${Thread.currentThread().name}")
specialTask()
println("2-2 = ${Thread.currentThread().name}")
}
}出力がこうです。specialTask()を抜けた後もspecialTask()が使っていたスレッド(Dispatchers)を使っています。が、あんまり使う機会はないと思います。
知っていることを自慢できるかもしれないけど、そんなもん自慢したら深堀りされて詰む。
1-1 = main
2-1 = main
1-2 = special thread dispatcher !!!
2-2 = special thread dispatcher !!!withContext { }、これまでも習ってないのにちょくちょくでてきてましたが。ついに触れます。
これを使うと好きなところでスレッド、正しくはDispatchersを切り替えることが出来ます。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// UI スレッドで処理させる
lifecycleScope.launch(Dispatchers.Main) {
changeThread()
}
}
private suspend fun changeThread() = coroutineScope {
withContext(Dispatchers.Default){
println("Default スレッド = ${Thread.currentThread().name}")
}
println("元のスレッド = ${Thread.currentThread().name}")
withContext(Dispatchers.Main){
println("Main スレッド = ${Thread.currentThread().name}")
}
println("元のスレッド = ${Thread.currentThread().name}")
}
}出力がこうです。
こんな感じにスレッドを行ったり来たり出来ます。newSingleThreadContext()も渡せます。
ブロック内は指定したスレッドで処理されます。ブロックを抜けると元のスレッドに戻るため、スレッドを切り替えているのにコールバックのようにネストせずに書けるのが感動ポイント。
Default スレッド = DefaultDispatcher-worker-1
元のスレッド = main
Main スレッド = main
元のスレッド = maindelay()のそれと同じように、この手の関数は、戻ってきた際に同じスレッドが使われるとは限らないので注意です。
先述の通りスレッド、ではなくDispatchersが元に戻るだけ、Dispatchers.Defaultは複数のスレッドを雇っているのでその時空いているスレッドが割り当てられます。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(Dispatchers.Default) {
changeThread("[1]")
}
lifecycleScope.launch(Dispatchers.Default) {
changeThread("[2]")
}
}
private suspend fun changeThread(tag:String) = coroutineScope {
println("$tag 1 = ${Thread.currentThread().name}")
withContext(Dispatchers.Main){
println("$tag 2 = ${Thread.currentThread().name}")
}
println("$tag 3 = ${Thread.currentThread().name}")
}
}これも実行するたびに変化するかもしれませんが、手元ではこんな感じに、Dispatchersこそ同じものの、スレッドは違うものが割り当てられてそうです。
[1] 1 = DefaultDispatcher-worker-1
[2] 1 = DefaultDispatcher-worker-2
[1] 2 = main
[2] 2 = main
[1] 3 = DefaultDispatcher-worker-3
[2] 3 = DefaultDispatcher-worker-2また、キャンセル後にサスペンド関数が呼び出せないのと同じように、withContext { }も呼び出せません。
ただし、キャンセルの章で話した通り、NonCancellableをつければ一応は使えます。乱用しないように。
構造化された並行性の章にて、親コルーチンは子コルーチンを追跡して、全部終わったことを確認した後に親が終わることを話しました。
しかし、コルーチンスコープを使うことでこの親子関係を切ることが出来ることも話しました。
この他にコルーチンコンテキストのJob()をオーバーライドすることでもこの関係を切ることが出来ます。
こんな感じに。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
val job = launch { otherJob() }
println("親を起動")
// 少し待った後、親をキャンセルする
delay(3_000)
println("キャンセル!")
job.cancelAndJoin()
}
}
private suspend fun otherJob() = coroutineScope {
launch {
println("Jobなし 開始")
delay(5_000)
println("Jobなし 5 秒まった")
}
launch(Job()) {
println("Job指定 開始")
delay(5_000)
println("Job指定 5 秒まった")
}
}
}親子関係が切れてしまったので、親がキャンセルされても生き残っていますね。使い道があるのかは知らない。
親を起動
Jobなし 開始
Job指定 開始
キャンセル!
Job指定 5 秒まった
Coroutine context and dispatchers | Kotlin
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html
コルーチンコンテキストは+ 演算子で繋げることが出来ます。
// UI スレッドで処理させる
lifecycleScope.launch {
try {
// キャンセルするかもしれない処理
} finally {
withContext(Dispatchers.Main + NonCancellable) {
// UI スレッドかつ、キャンセル時も実行したい場合
}
}
}もし、自分でコルーチンスコープを作って管理する場合は、ちゃんと使わなくなった際にコルーチンスコープを破棄するようにする必要があります。
例えばAndroidのサービスでコルーチンが使いたい場合、(Activity、Fragment、Jetpack Composeとは違い)Androidでは用意されていないため自分で用意する必要があります。
作る分にはCoroutineScope()やMainScope()を呼べば作れますが、ちゃんと破棄のタイミングでキャンセルしてあげる必要があります。
MainScope()のドキュメントに書いてあるので読んで。class AndroidService : Service() {
// Android の Service だと Kotlin Coroutines のためのコルーチンスコープがない。ので自分で用意する
private val scope = MainScope() // もしくは CoroutineScope()
override fun onBind(intent: Intent?): IBinder? {
return null
}
// Service が使われなくなったとき
override fun onDestroy() {
super.onDestroy()
// コルーチンスコープも破棄する
scope.cancel()
}
}
Coroutine context and dispatchers | Kotlin
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html
AndroidではHandlerを引数に渡す関数が結構あります。
この手の関数は引数にコールバックと、そのコールバックを呼び出すスレッドを指定するためのHandlerを取ります。
ちなみにnullのときはメインスレッド、コードだとHandler(Looper.getMainLooper())をAndroid側で渡しているそうです。
まあこの手の関数はHandlerをnullに出来て、しかも大抵はnullで問題なかったりするのですが。(小声)
例えばCamera2 APIの写真撮影メソッド。nullでメインスレッド?
capture(frontCameraCaptureRequest, object : CameraCaptureSession.CaptureCallback {
// ...以下省略
}, Handler(handlerThread.looper))例えばMediaCodecのsetCallback()。nullでメインスレッド
setCallback(object : MediaCodec.Callback() {
// ...以下省略
}, Handler(handlerThread.looper))例えばMediaProjectionのregisterCallback()。nullでメインスレッド
registerCallback(object : MediaProjection.Callback() {
// ...以下省略
}, Handler(handlerThread.looper))ただ、コールバックを別スレッドで呼び出されるようHandler()とHandlerThread()を作ることもあるでしょう。MediaCodecの非同期モードは多分そう。
概要としては、HandlerThread()と呼ばれるスレッドを作り、Handler#getLooper()をHandler()へ渡して、出来たHandlerを引数に渡せば完成なのですが、もう一歩、このHandler()をKotlin CoroutinesのDispatchersとしても使うことが出来ます。
こんな感じですね。HandlerThread()をKotlin Coroutinesでも転用したい場合はどうぞ!
class MainActivity : ComponentActivity() {
private val handlerThread = HandlerThread("handler_thread").apply { start() }
private val handler = Handler(handlerThread.looper)
private val handlerDispatcher = handler.asCoroutineDispatcher()
}これの答えをKotlin Coroutinesの中で探したんですが、それっぽい記述を見つけることは出来ず。
というわけで手詰まりになりました。ここから先の話は憶測です。ドキュメントには書いていない話なので。
Kotlin Coroutines関係なく、CPUを駆使する処理(CPU バウンドなタスク)だと、起動するスレッド数は搭載されているCPUのコア数と同じか半分が良いという風の噂があるらしい。
Number of thread: Computation intensive vs IO intensive operations?
I came across below statement at this blog Computation intensive operations should use a number of threads lower than or equal to the number of cores, while IO intensive operations like copying f...
https://softwareengineering.stackexchange.com/questions/375929/number-of-thread-computation-intensive-vs-io-intensive-operations
Number of processor core vs the size of a thread pool
Many times I've heard that it is better to maintain the number of threads in a thread pool below the number of cores in that system. Having twice or more threads than the number of cores is not onl...
https://stackoverflow.com/questions/14556037/number-of-processor-core-vs-the-size-of-a-thread-pool
あんまり理解できてないけど、なんとなくこういうこと?かなり理論上な気はする。
CPUを駆使する処理はCPUにしか依存していないので(遅くなる原因のファイル読み書き等の邪魔するものがない場合)1スレッドだけで使用率を100%に出来る?。
1スレッドだけで100%になるからコア数以上に増やしてもそれ以上、100%を超えられないので意味がないってことらしい?
一方ファイル読み書きやインターネット通信(IO バウンド)はCPUがどれだけ早くても読み書き、通信速度がボトルネックで100%にできない?
から並列化させてCPUを遊ばせない(暇させない)ほうがいいってことなのかな
↓書きました↓
省略します!!!
やる気があればFlowの話をします。というのもFlowはサスペンド関数のそれ同じくらい内容があってそれだけで記事一本出来てしまうので。。。
いやだって目次が多すぎる。。。
例外の話です。地味に新しい事実がある
違う親の子コルーチンというか、自分が親のコルーチンのときにも言える話ですね。lifecycleScope.launch { }のこと。
まあ落ちます。自分が親のコルーチンにも言えることでしょうが。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
try {
// this の Scope を使わずに起動
val parent = lifecycleScope.launch {
delay(3_000)
throw RuntimeException()
}
parent.join()
} catch (e: RuntimeException) {
println("RuntimeException をキャッチ") // 出来ません
}
}
}
}Process: io.github.takusan23.kotlincoroutienspractice, PID: 24548
java.lang.RuntimeException
at io.github.takusan23.kotlincoroutienspractice.MainActivity$onCreate$1$parent$1.invokeSuspend(MainActivity.kt:26)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:235)ちなみにasync { }は受け取れますが、そもそも違う親で起動しない気がして、だから何みたいな。。。。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
// this の Scope を使わずに起動
val parent = lifecycleScope.async {
delay(3_000)
throw RuntimeException()
}
try {
parent.await()
} catch (e: RuntimeException) {
println("RuntimeException をキャッチ") // できる。けどあんまりないハズ
}
}
}
}キャッチされなかった例外を観測することが出来ます。キャッチされなかったというわけで、もうアプリは回復できません(落ちてしまう)
スタックトレース収集とかに使えるかも?
試したことがあるかもしれませんが、launch { }をtry-catchで囲っても意味がありません。普通に落ちます。
軽量スレッドとはいえスレッドなんですから。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
try {
lifecycleScope.launch {
delay(3_000)
throw RuntimeException()
}
} catch (e: Exception) {
println("launch { } で例外をキャッチ") // 動かない
}
}
}じゃあどうすればいいのかというと、CoroutineExceptionHandler { }を使うと出来ます。
先述の通りキャッチされなかった例外が来るので、回復目的には使えません。出来るとしたらスタックトレースと回収とかアプリ全体を再起動とかでしょうか。
ここでキャッチ出来るようになるので、アプリが落ちることはなくなります。
例えば以下のようにスタックトレースの回収が出来ます。
class MainActivity : ComponentActivity() {
private val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
// https://stackoverflow.com/questions/9226794/
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
throwable.printStackTrace(printWriter)
stringWriter.flush()
println("!!! CoroutineExceptionHandler !!!")
println(stringWriter.toString())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(coroutineExceptionHandler) {
delay(3_000)
throw RuntimeException()
}
}
}logcatはこんな感じです。
System.out io....an23.kotlincoroutienspractice I !!! CoroutineExceptionHandler !!!
System.out io....an23.kotlincoroutienspractice I java.lang.RuntimeException
System.out io....an23.kotlincoroutienspractice I at io.github.takusan23.kotlincoroutienspractice.MainActivity$onCreate$1.invokeSuspend(MainActivity.kt:39)
System.out io....an23.kotlincoroutienspractice I at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
System.out io....an23.kotlincoroutienspractice I at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:235)ちなみに親以外に付けても動きませんので!
lifecycleScope.launch {
launch(coroutineExceptionHandler) { // 無意味!
delay(3_000)
throw RuntimeException()
}
}子コルーチンの中で例外が投げられた場合、親を通じて他の子もキャンセルされるわけですが、子に来るのはキャンセル例外です。
キャンセルの原因となった子コルーチンの例外は親のコルーチンで受け取れます。
子コルーチンがRuntimeExceptionでコケたら他の子コルーチンにはRuntimeExceptionが来るわけではなく、キャンセルの例外が来るよという話です。親でRuntimeExceptionを受け取れます。

Coroutine exceptions handling | Kotlin
https://kotlinlang.org/docs/exception-handling.html
ドキュメントにあるサンプルコードパクってもAndroidだと動かないな、、、
ここまで出てきたコードでは子のコルーチンが例外投げられたら、親に伝搬し、他の子に対してはキャンセルを投げるというものでした。
しかし、他の子コルーチンは生かしておきたい場合があります。その場合、子コルーチン内の処理をtry-catchして例外を投げないようにする、でも良いですが、もう一つの方法があります。SupervisorJob()とsupervisorScope { }です。
試してみましょう。
指定時間後に成功するタスクsuccessTask()と、指定時間後に例外を投げて失敗するfailTask()をそれぞれ並列で呼び出します。
async { }はawait()で値もしくは例外を受け取ることが出来ます。って書いてあります。class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
supervisorTask()
println("supervisorScope { } complete !!!")
}
}
private suspend fun supervisorTask() = supervisorScope {
// 失敗する子を含んで並列起動
val async1 = async { successTask() }
val async2 = async { successTask() }
val async3 = async { failTask() }
listOf(async1, async2, async3).forEachIndexed { index, deferred ->
try {
// await() で例外をキャッチできる
println("[$index] await()")
println(deferred.await())
} catch (e: RuntimeException) {
println("[$index] RuntimeException キャッチ")
}
}
}
private suspend fun successTask(): String {
delay(3_000)
return "hello world"
}
private suspend fun failTask() {
delay(2_000)
throw RuntimeException()
}
}結果はこんな感じで、supervisorScope { }は子と子が持っている子にのみキャンセルが伝搬します。親には伝搬しません。
ので、親に対して明示的にキャンセルしない場合は、子コルーチンはそのまま生き続けます。
もちろん子がすべて終わるまで親が終わらないルールはsupervisorScope { }でも引き継がれています。なので最後にcomplete !!!が出る形にあります。
[0] await()
hello world
[1] await()
hello world
[2] await()
[2] RuntimeException キャッチ
supervisorScope { } complete !!!さっきはasync { }の例を出しましたが、launch { }でも動きます。async { }だとawait()でキャッチできますが、launch { }の場合はというと、、、、代わりにコルーチンスコープに設定されたCoroutineExceptionHandlerで例外をキャッチできます。コルーチンスコープにそれがない場合は落ちます。
class MainActivity : ComponentActivity() {
private val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
throwable.printStackTrace(System.out) // System.out に出す
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch(coroutineExceptionHandler) { // CoroutineExceptionHandler をつける
supervisorTask()
println("supervisorScope { } complete !!!")
}
}
private suspend fun supervisorTask() = supervisorScope {
launch { println(successTask()) }
launch { failTask() }
}
private suspend fun successTask(): String {
delay(3_000)
return "hello world"
}
private suspend fun failTask() {
delay(2_000)
throw RuntimeException()
}
}これでもsuccessTask()がちゃんと生き残っています。
java.lang.RuntimeException
at io.github.takusan23.kotlincoroutienspractice.MainActivity.failTask(MainActivity.kt:47)
at io.github.takusan23.kotlincoroutienspractice.MainActivity.access$failTask(MainActivity.kt:19)
at io.github.takusan23.kotlincoroutienspractice.MainActivity$failTask$1.invokeSuspend(Unknown Source:14)
... 以下省略
hello world
supervisorScope { } complete !!!見出しは適当につけました。
最後の章。かな。
マルチスレッドで変数を同時に書き換えると正しい値にならないというのは有名だと思います。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
var count = 0
lifecycleScope.launch(Dispatchers.Default) {
coroutineScope {
repeat(10_000){
launch {
count++
}
}
}
println("結果 = $count")
}
}
}logcatはこうです。実行するタイミングによっては10000が表示されるかもしれませんが、それはたまたま動いただけです。
これはマルチスレッドで同時に変数にアクセスしているから(まあ目には見えない速さなんですが)、タイミング悪く増える前の変数に+1したとかでおかしくなってるんでしょう。
結果 = 9995Dispatchersの章で話した通り、Dispatchers.MainやシングルスレッドのDispatchersを使えば、他スレッドから参照されたり変更されたりしないので、ちゃんと期待通りになります。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Main とか
lifecycleScope.launch(Dispatchers.Main) {
increment()
}
// newSingleThreadContext() とか
lifecycleScope.launch {
newSingleThreadContext("single_thread_dispatchers").use { dispatcher ->
withContext(dispatcher) {
increment()
}
}
}
}
private suspend fun increment(){
var count = 0
coroutineScope {
repeat(10_000) { // 10_000 回ループする
launch {
count++ // +1 する
}
}
}
println("結果 = $count")
}
}また、変数に@Volatileを付けても期待通りにならないそうです(これがなんなのかいまいちわからないのでスルーします)
ちなみにrepeat { }の中でwithContext { }よりもwithContext { }の中でrepeat { }のほうが良いです。
いくら軽いとはいえ、繰り返し文のたびにwithContext { }を呼び出すのはコストがかかるそうです。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
var count = 0
val time1 = measureTimeMillis {
repeat(10_000) {
withContext(Dispatchers.Default) {
count++ // 特に意味のないことをしてます。withContext の呼び出しが地味に重いので、time2 のようにループ開始前に呼び出しておくこと
}
}
}
val time2 = measureTimeMillis {
withContext(Dispatchers.Default) {
repeat(10_000) {
count++
}
}
}
println("ループの中で withContext = $time1")
println("withContext の中でループ = $time2")
}
}
}ループの中で withContext = 716
withContext の中でループ = 0いくつかあります。
まずはAtomicInteger、これはIntですが他にもBooleanとかもあるはず。何回実行しても10000になります。
val count = AtomicInteger()
lifecycleScope.launch(Dispatchers.Default) {
coroutineScope {
repeat(10_000){ // 10_000 回ループする
launch {
count.incrementAndGet() // +1 する
}
}
}
println("結果 = ${count.get()}")
}あとはsynchronizedのコルーチン版Mutex()を使うことでも同時アクセスを防ぐためにロックできます。変数の操作はもちろん、処理自体を同時実行されないようにしたい場合にこちら。ブロック内はスレッドセーフになります。
なおsynchronizedはKotlin Coroutinesでは使えません。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val mutex = Mutex()
lifecycleScope.launch(Dispatchers.Default) {
var count = 0
coroutineScope {
repeat(10_000) { // 10_000 回ループする
launch {
mutex.withLock { // 書き換わらないようロック
count++ // +1 する
}
}
}
}
println("結果 = $count")
}
}
}結果 = 10000(FlowとChannel以外は)一通り読み終えました。
長かった。。。。つかれた。
ここからはドキュメントに書いてないけど、実戦投入する際によく使うやつを書いていきます。
ちなみに、これが使えるのは一度だけ値を返す場合のみです。複数回コールバック関数が呼ばれる場合はFlowの勉強が必要です。。。
コールバックの関数をサスペンド関数に変換できます。Kotlin Coroutinesを使ってやりたいことの1位か2位くらいに居座っていそう、コールバックの置き換え。達成感あるんだよなこれ。suspendCoroutine { }とsuspendCancellableCoroutine { }の2つがあります。差はキャンセル不可能か可能かです。後者のキャンセル対応版を使うのが良いです。
suspendCoroutine { }の場合はこんな感じです。suspendCoroutineでContinuationがもらえるので、コールバックの中でresumeとかを呼び出せば良い。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
request()
}
}
private suspend fun request() {
val response = awaitOkHttpAsyncCallback()
println(response)
}
private suspend fun awaitOkHttpAsyncCallback() = suspendCoroutine { continuation ->
val request = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
OkHttpClient().newCall(request).enqueue(object : Callback { // コールバックの関数
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e) // 失敗時
}
override fun onResponse(call: Call, response: Response) {
continuation.resume(response.body!!.string()) // 成功時
}
})
}
}キャンセル出来ないので、キャンセルを要求したとしても、suspendCoroutine自身はキャンセル例外を投げません。
後続でキャンセルチェックとかを入れるなどする必要があります。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
val job = launch { request() }
// 500ms 後にキャンセル
delay(500)
job.cancel()
}
}
private suspend fun request() = coroutineScope { // ensureActive() のため
try {
val response = awaitOkHttpAsyncCallback() // キャンセル不可なので、非同期処理中にキャンセルが要求されても自身は例外を投げない
ensureActive() // キャンセルチェック。これをコメントアウトすると println() に進んでしまう...
println(response)
} catch (e: CancellationException) {
println("CancellationException !!!")
throw e
}
}ensureActive()がキャンセル判定をし、今回だとキャンセルしているので例外を投げます。logcatにはCancellationException !!!が出ます。ensureActive()をコメントアウトすると、そのままprintln()へ進んでしまいます。これはおそらく意図していない操作だと思います。
suspendCancellableCoroutineだとこんな感じです。
こちらはキャンセル機能を持ちます。ContinuationがCancellableContinuationに変化します。キャンセルが要求されたときに呼び出されるコールバックを提供します。
極力こちらのsuspendCancellableCoroutine { }でキャンセルに協力的なサスペンド関数を作っていくのがよろしいかと思います。
使い方はキャンセルに対応したコールバックが追加されたくらいで、ほぼ同じです。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
request()
}
}
private suspend fun request() {
val response = awaitCancellableOkHttpAsyncCallback()
println(response)
}
private suspend fun awaitCancellableOkHttpAsyncCallback() = suspendCancellableCoroutine { continuation ->
val request = Request.Builder().apply {
url("https://example.com/")
get()
}.build()
val call = OkHttpClient().newCall(request)
call.enqueue(object : Callback { // コールバックの関数
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e) // 失敗時
}
override fun onResponse(call: Call, response: Response) {
continuation.resume(response.body!!.string()) // 成功時
}
})
// コルーチンがキャンセルされたらリクエストをキャンセルさせます
continuation.invokeOnCancellation {
call.cancel()
}
}
}先述の通り、キャンセルに対応しているので、キャンセル要求がされた場合はsuspendCancellableCoroutine { }自身が例外を投げます。
そのためキャンセルチェックは不要です。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
val job = launch { request() }
// 500ms 後にキャンセル
delay(500)
job.cancel()
}
}
private suspend fun request() {
try {
val response = awaitCancellableOkHttpAsyncCallback() // キャンセル対応なので、キャンセルが要求されたら自身が例外を投げる
println(response)
} catch (e: CancellationException) {
println("CancellationException !!!")
throw e
}
}ちゃんとlogcatにはensureActive()無しでCancellationException !!!が出ています。
同時に処理を実行する際に出てくるキーワードに、 並列(parallel / パラレル) と 並行(concurrent / コンカレント) という単語が出てきます。
機械翻訳にぶち込むと同じ単語が出てきて困惑するのですが、 少なくともKotlin Coroutinesでは別に扱ってそうです。
これは同時に起動は出来るというだけ。同時に処理はできていない。CPUが1コアしかなければ1つのことしか出来ない。。。。ですが、実はコンテキストスイッチと呼ばれるものがあって、
同時に起動している処理を細かく区切って、それぞれ均等にCPUに与えて処理させているので、同時に処理が出来ているように見えている。
1コアしか無いので、同じ時間には1つしか処理できないことになります。
ワンオペで店を回しているようなものでしょうか。
これは同時に処理が出来ます。マルチコア CPUとかIntel のハイパースレッディングのそれを使うやつです。
同時、同じ時間に複数の処理が出来る違いがあります。もちろんコア数を超えるスレッドがあれば同じくコンテキストスイッチが頑張ります。
複数人雇っていることになりますね。これなら同じ時間に違う仕事をさせることが出来ます。
並列の実行数を制限したいときに、どっちを制限したいのかによって使い分ける必要があります。
| 平行(同時に起動する) | 並列(同時に処理する。使うスレッド数を制限する) | |
|---|---|---|
| 1つだけ | Mutex() | limitedParallelism(1) |
| 上限付き | Semaphore(上限の数) | limitedParallelism(上限の数) |
例えば以下の関数はwithLock { }の中からのみ呼び出されているため、successTask()を3つ並列にしても、1つずつ処理されることになります。
なので、同時にHello worldが出力されることはありません。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val mutex = Mutex()
lifecycleScope.launch {
mutex.withLock { successTask() }
}
lifecycleScope.launch {
mutex.withLock { successTask() }
}
lifecycleScope.launch {
mutex.withLock { successTask() }
}
}
private suspend fun successTask() {
delay(3_000)
println("Hello world")
}
}logcat
03:53:59.277 10572-10572 System.out io....an23.kotlincoroutienspractice I Hello world
03:54:02.280 10572-10572 System.out io....an23.kotlincoroutienspractice I Hello world
03:54:05.281 10572-10572 System.out io....an23.kotlincoroutienspractice I Hello worldMutex()は1つずつですが、2個までは許容して3個目以降は待ち状態にしたい場合があると思います。
例えばMinecraftのマインカートは一人乗りなのでMutex()で事足りますが、ボートは二人まで乗れますのでMutex()は使えないですね。
それがSemaphore()です。見ていきましょう。
引数に同時に利用できる上限数を入れます。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val semaphore = Semaphore(2)
lifecycleScope.launch {
semaphore.withPermit { successTask() }
}
lifecycleScope.launch {
semaphore.withPermit { successTask() }
}
lifecycleScope.launch {
semaphore.withPermit { successTask() }
}
lifecycleScope.launch {
semaphore.withPermit { successTask() }
}
}
private suspend fun successTask() {
delay(3_000)
println("Hello world")
}
}実行結果ですが、2つずつ処理されるので、logcatの出力も2個ずつ出てくるんじゃないかなと思います。
04:02:47.409 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:47.411 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:50.412 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:50.413 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:53.416 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:53.417 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:56.421 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:56.422 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:59.425 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello world
04:02:59.426 12167-12167 System.out io....an23.kotlincoroutienspractice I Hello worldこれは同時に利用するスレッド数を制限するものです。Dispatchers.IOやDispatchers.Defaultは複数のスレッドを持っている(雇っている)ので、それらの制限をするのに使うそう。
ややこしいのが、これはスレッド数を制限するものであって、launch { }やasync { }の同時起動数を制限するものではないということです。Kotlin Coroutinesはスレッドを有効活用するため、delay()等の待ち時間が出れば他のコルーチンの処理に当てるのでコルーチンの起動数制限には使えません。
もしlaunch { }やasync { }の同時起動数を制限したい場合はさっきのSemaphore()が解決策です。
例えばlimitedParallelism(1)にした場合はスレッドが1つだけになるので、このようにループでインクリメントさせても正しい値になります。
ちなみにシングルスレッドが約束されるだけであって同じスレッドが使われるとは言っていません。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val singleDefault = Dispatchers.Default.limitedParallelism(1)
var count = 0
lifecycleScope.launch {
withContext(singleDefault){
repeat(10_000){
launch {
count++
}
}
}
println("シングルスレッドなので $count")
}
}
}うーん、どこで使うのかな。多分賢い使い方があるんだと思うんだけど。
JavaScriptから来た方向け。KotlinのforEachやmap等でサスペンド関数を呼び出せるよという話です。JavaScriptではforEach内でawait出来ないのは有名な話ですが、Kotlinでは出来るよという話、それだけです。
Kotlinにはインライン関数と呼ばれる、関数呼び出しではなく、関数の中身のコードを呼び出し元に展開する機能があります。(他の言語にあるマクロみたいなやつ)forEachやmapはインライン関数なので、これらは純粋な繰り返し文に置き換わります。なのでサスペンド関数を呼び出すことが出来る感じです。
例えば以下のKotlinコードは
val list = listOf("Pixel 9", "Pixel 9 Pro", "Pixel 9 Pro XL", "Pixel 9 Pro Fold")
list.forEach { text ->
println(text)
}Javaではこのようになるそうです。
ちゃんと純粋なループに置き換わっていますね。
String[] var3 = new String[]{"Pixel 9", "Pixel 9 Pro", "Pixel 9 Pro XL", "Pixel 9 Pro Fold"};
List list = CollectionsKt.listOf(var3);
Iterable $this$forEach$iv = (Iterable)list;
int $i$f$forEach = false;
Iterator var5 = $this$forEach$iv.iterator();
while(var5.hasNext()) {
Object element$iv = var5.next();
String text = (String)element$iv;
int var8 = false;
System.out.println(text);
}サスペンド関数紹介ドラゴンやります。
個人的好きな関数 awaitCencellalation()
これは、コルーチンが終了するまで一時停止する関数です。例えば、コールバックをsuspendCoroutine / callbackFlow { }に変換するまでもないときに使えます。
lifecycleScope.launch {
val lifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
}
}
try {
lifecycle.addObserver(lifecycleObserver) // コールバック登録
awaitCancellation() // 待つ
} finally {
lifecycle.removeObserver(lifecycleObserver) // コルーチン終了時(lifecycleScope 終了時)解除
}
}例に出したところ申し訳ないですが、lifecycle ライブラリは自動でコールバックを解除してくれるらしいので、登録しっぱなしでいい。らしい
これをつかうとDisposableEffect相当の処理をLaunchedEffectで実現できます。メリットですが、後者だとサスペンド関数が呼べるおまけ付き
// DisposableEffect はサスペンド関数が呼べない
DisposableEffect(key1 = Unit) {
onDispose { }
}
// LaunchedEffect はサスペンド関数が呼び出せる。ついでに終了昨日もつければ、onDispose 相当の処理にもなる
LaunchedEffect(key1 = Unit) {
try {
// サスペンド関数を呼び出せる
awaitCancellation()
} finally {
// onDispose 相当の処理
}
}こっちは有名ですが、コルーチン終了時(launch { }のJobが終了した時)に呼び出されるコールバックです。
やっぱり最後はコールバックなんだって。
lifecycleScope.launch {
delay(1_000)
}.invokeOnCompletion {
// コルーチン終了時
}いやーーーーーーKotlin Coroutines、頭が良い!よく考えられてるなと思いました。