たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 43198
目次
本題
環境
はじめに
あらずし
ライブラリの解説
コールバック
なぜコールバック
コールバックの代替案 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 でサスペンド関数呼べます
おわりに
どうもこんばんわ。
あまいろショコラータ1・2・3 コンプリートパックを買ってやっています。買ったのは3の発売のときだったので積んでたことになりますね、、
まずは、あまいろショコラータ攻略しました。
みくりちゃん!!!ところどころにあるやりとりがおもしろかった
↑じとめすち
むくれてるのかわい~
英語分からん分かる、英語が第一言語だとやっぱドキュメントもエラーメッセージもそのまま分かるのでしょうか。
直でドキュメント読めるのずるいとおもう(?)
><
それはそうと服が似合ってていい
あとあんまり関係ないけど無印版のエンディング曲がシリーズの中で一番好きかもしれません、
あ!!!!!これ
このブログの土日祝日のアクセス数のこと言ってますか!??!?!?!
技術(?)ブログ、休みの日はあんまりお客さん来てない。
CloudFront
の転送量をCloudWatch
で見てみたけど、明らかに休みの日だけ折れ線グラフがガタ落ちしてる。面白い。
Jetpack Compose
がぶいぶい言わせている今日、Jetpack Compose
ではありとあらゆるところでKotlin Coroutines
のサスペンド関数や、Flow
が多用されています。
(Android
はかなり)コールバック地獄だったので、他の言語にあるような同期スタイルで記述でき、とても嬉しいわけですが、、、、
その割にはAndroid
本家のKotlin Coroutines
の紹介がおざなりというか、分かる人向けにしか書いていません!(別にAndroid
側が紹介する義理なんて無いけど)
Jetpack Compose
であれだけ多用しているのに!?!?(いや関係ないだろ)
https://developer.android.com/kotlin/coroutines
かくいう私もあんまり理解できてない、、、なんとなくで書いている。
数年前からふいんき(なぜか変換できない)で書いているので真面目にやります。
https://takusan.negitoro.dev/posts/android_sqlite_to_room/#なんでコルーチン
というわけで今回はKotlin Coroutines
のドキュメントを読んでみようの会です。ぜひ夏休みの読書感想文にどうぞ。(てか世間は夏休みなのか)
https://kotlinlang.org/docs/coroutines-guide.html
Kotlin Coroutines
の話をするのに、Android
である必要はないんですが、私がサンプルコード書くのが楽というだけです。
特別なことがなければAndroid
じゃない環境、Kotlin/JVM
とかでも転用できます。
いちおうドキュメントの流れに沿って読んでいこうかなと思うのですが、
スレッドと違い、なぜコルーチンは欲しいときに欲しいだけ作っていいのかとかの話をしないと興味がわかないかなと思いそこだけ先にします。
多分合ってると思う、間違ってたらごめん。
まあ分かりやすいと思うのでAndroid
で、あとはJetpack Compose
を使います。
もしサンプルコードを動かすなら↓
サンプルとしてインターネット通信が挙げられると思うので、OkHttp
というHTTP クライアントライブラリ
も入れます。
インターネット通信のサンプルがあるため、インターネット権限を付与したプロジェクト(アプリ)を作っておいてください。
https://kotlinlang.org/docs/coroutines-guide.html
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 本
に書いてありました。→ https://azu.github.io/promises-book/#what-is-promise )
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
成功 9
Kotlin Coroutines
はこれのKotlin
版です。async function
はsuspend fun
になります。
というわけで長い長いあらずしも終わり。いよいよ本題にいきましょう。
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
兎にも角にもなにか書いてみましょう。というわけでこちら。
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
したとしても、上記の理由によりびくともしないと思います。
ただ、上記の説明は、既存のスレッドを効率良く使う説明であって、
大量に作ってもいい理由にはあんまりなっていない気がします。
https://stackoverflow.com/questions/63719766/
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
のスレッドを作っているわけじゃないだけあって、いっぱい押しても特に起きない
https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency
コルーチンのドキュメントをいい加減なぞっていこうかと思ったのですが、
もう一個、これはスレッド
、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
のドキュメントを見に行きましょう。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/
読んでみると、どうやらcoroutineScope { }
を使えば、suspend fun
の中でもlaunch { }
出来るっぽいですよ!?
試してみると、確かにcoroutineScope { }
ではエラーが消えています。
private suspend fun downloadFile() {
coroutineScope {
launch { } // 祝!これで起動できた!!!
}
}
これは何故かと言うと、launch { }
がcoroutineScope
の拡張関数になっているからですね。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
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
に構造化された並行性を導入した方の、こちらの記事もどうぞ
https://elizarov.medium.com/structured-concurrency-722d765aa952
↑の方の記事で引用されているこちらも。
コールバックはgoto
文と何ら変わらないという話
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
ちな
D.C.5
いや、上記の画像はあんまり関係ないのですが、
二人三脚という競技は、例えば解けてしまった場合は結び直して再出発する必要があります。
いきなり相方がどっか走り出したらルール違反になります。
おおむね、プログラミングの世界でも、起動した並列処理がどこか行方不明にならないよう待ち合わせしたり、
時にはエラーになったら他の並列処理を終了に倒したいときがあります。分割ダウンロードを作るとか。
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
https://kotlinlang.org/docs/cancellation-and-timeouts.html
これも大事で、これを守らないと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}")
}
}
自分でコルーチンスコープを作る場合、MainScope()
やCoroutineScope()
を呼び出せば作れるのですが、それはコルーチンコンテキストとディスパッチャの章
で!
#コルーチンスコープ
cancel()
したあとにjoin()
することで終わったことを確認できます。別にキャンセルしなくても終わるまで待ちたければjoin()
すれば良いです。
cancel()
はあくまでもキャンセルを命令するだけで、キャンセルの完了を待つ場合はjoin()
が必要です。
後述しますが、キャンセルをハンドリングして後始末をする事ができるため、その後始末を待つ場合はjoin()
が役立つかも。
また、cancelAndJoin()
とかいう、名前通り2つを合体させた関数があります。
まずはキャンセルの仕組みをば。
キャンセルの仕組みですが、キャンセル用の例外CancellationException
をスローすることで実現されています。
キャンセルが要求された場合は上記の例外をスローするわけですが、誰がスローするのかと言うと、一時停止中のサスペンド関数ですね。以下の例だとdelay()
さんです。
while (isActive) { // isActive はキャンセルしたら false になるよ
delay(1_000) // キャンセルが要求されたらキャンセル例外を投げるよ!
println("loop ...")
}
例えば先程のカウントアップのプログラムでは、delay()
がキーパーソンになります。
delay()
のドキュメントを確認しますが、指定時間待っている間にコルーチンがキャンセルされた場合は関数自身がキャンセル例外をスローすると書いています。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html
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 { }
を作ってくれないのが悪い気もする。
議論されてるけど、、、まーだ時間かかりそうですかねー?:https://github.com/Kotlin/kotlinx.coroutines/issues/1814
対策としてはrunCatching { }
のキャンセル対応版を作るか
参考にしました、ありがとうございます: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#isActive
CoroutineScope#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
を大量に消費する処理(フィボナッチ数列を計算する)とかの、
重たい、時間がかかる処理を始める前に確認すればいいんじゃないかなと思います。
https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ja#coroutine-cancellable
それから、delay()
やそのほかkotlinx.coroutines
パッケージ傘下にあるサスペンド関数(最初から用意されているサスペンド関数)は、基本的にキャンセルに対応しているため、ensureActive()
とかで確認せずともキャンセルされたら例外を投げてくれるはずです。
逆を言えば最初から用意されていない、自分でサスペンド関数を書く場合はキャンセル出来るよう心がける必要があります。
delay
withContext
(後述)
join
/ cancelAndJoin
https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629
確かに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 { }
がキャンセルに対応しています。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
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 { }
以外で使ってはいけません。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-non-cancellable/
タイムアウトもできます。
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
タイムアウト例外が出たら動かない
これは元ネタがあって、私はただパクっただけです、興味があれば先に元ネタを読んでください。
https://medium.com/better-programming/the-silent-killer-thats-crashing-your-coroutines-9171d1e8f79b
回答: なりません
ensureActive()
の説明では以下のコードと大体同じことをやっていると書いています。
これだけ見ると、例外を投げればキャンセルできるのかと思ってしまいます。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html
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
コードからその例外を投げられる可能性は、、、可能性だけならありますね。
https://kotlinlang.org/docs/composing-suspending-functions.html
つぎはこれ、サスペンド関数の話です。ついに並列処理の話ができます。
今まで通り、そのまま書けば直列処理です。
普通に書くだけで順番通りに処理されるとか、コールバックのときにはあり得なかったことですね。
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 で完了
は最初の方で話したので、まずはこちらを読んでください。Promise / Future
との違いの話です。
#構造化された並行性
で、上記では触れなかった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 をキャッチ
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html
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
向け)に拡張して作られたからです
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
実際に指定してみます。以下のコードを試してみましょう。
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-1
Android
の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.immediate
lifecycleScope
や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
元のスレッド = main
delay()
のそれと同じように、この手の関数は、戻ってきた際に同じスレッドが使われるとは限らないので注意です。
先述の通りスレッド、ではなく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 秒まった
本家の説明で十二分だとおもう
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html#naming-coroutines-for-debugging
コルーチンコンテキストは+ 演算子
で繋げることが出来ます。
// UI スレッドで処理させる
lifecycleScope.launch {
try {
// キャンセルするかもしれない処理
} finally {
withContext(Dispatchers.Main + NonCancellable) {
// UI スレッドかつ、キャンセル時も実行したい場合
}
}
}
もし、自分でコルーチンスコープを作って管理する場合は、ちゃんと使わなくなった際にコルーチンスコープを破棄するようにする必要があります。
例えばAndroid
のサービスでコルーチンが使いたい場合、(Activity
、Fragment
、Jetpack Compose
とは違い)Android
では用意されていないため自分で用意する必要があります。
作る分にはCoroutineScope()
やMainScope()
を呼べば作れますが、ちゃんと破棄のタイミングでキャンセルしてあげる必要があります。
という話がMainScope()
のドキュメントに書いてあるので読んで。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
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()
}
}
すいませんこれは使ったこと無いので分かりません。;;
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html#thread-local-data
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
としても使うことが出来ます。
(ということを最近知りました、ありがとうございます)
https://qiita.com/sdkei/items/b066817fb7f7c34d5760
こんな感じですね。
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
のコア数と同じか半分が良いという風の噂があるらしい。
あんまり理解できてないけど、なんとなくこういうこと?かなり理論上な気はする。
CPU
を駆使する処理はCPU
にしか依存していないので(遅くなる原因のファイル読み書き等の邪魔するものがない場合)1スレッドだけで使用率を100%
に出来る?。
1スレッドだけで100%
になるからコア数以上に増やしてもそれ以上、100%
を超えられないので意味がないってことらしい?
一方ファイル読み書きやインターネット通信(IO バウンド
)はCPU
がどれだけ早くても読み書き、通信速度がボトルネックで100%
にできない?
から並列化させてCPU
を遊ばせない(暇させない)ほうがいいってことなのかな
https://kotlinlang.org/docs/flow.html
↓書きました↓
https://takusan.negitoro.dev/posts/amairo_kotlin_coroutines_flow/
省略します!!!
やる気があれば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
を受け取れます。
はドキュメントにゆずります。得にはないかな、、、
https://kotlinlang.org/docs/exception-handling.html#exceptions-aggregation
ドキュメントにあるサンプルコードパクってもAndroid
だと動かないな、、、
ここまで出てきたコードでは子のコルーチンが例外投げられたら、親に伝搬し、他の子に対してはキャンセルを投げるというものでした。
しかし、他の子コルーチンは生かしておきたい場合があります。その場合、子コルーチン内の処理をtry-catch
して例外を投げないようにする、でも良いですが、もう一つの方法があります。SupervisorJob()
とsupervisorScope { }
です。
試してみましょう。
指定時間後に成功するタスクsuccessTask()
と、指定時間後に例外を投げて失敗するfailTask()
をそれぞれ並列で呼び出します。
async { }
はawait()
で値もしくは例外を受け取ることが出来ます。って書いてあります。
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
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 !!!
見出しは適当につけました。
最後の章。かな。
https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html
マルチスレッドで変数を同時に書き換えると正しい値にならないというのは有名だと思います。
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
したとかでおかしくなってるんでしょう。
結果 = 9995
Dispatchers
の章で話した通り、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 !!!
が出ています。
https://stackoverflow.com/questions/1050222/
同時に処理を実行する際に出てくるキーワードに、 並列(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 world
Mutex()
は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);
}
いやーーーーーーKotlin Coroutines
、頭が良い!よく考えられてるなと思いました。