[解決済み] Kotlin: withContext() vs Async-await
疑問点
私はこれまで kotlin ドキュメント を読みましたが、私が正しく理解していれば、2つのKotlinの関数は次のように動作します。
-
withContext(context)
: 現在のコルーチンのコンテキストを切り替え、与えられたブロックが実行されると、コルーチンは以前のコンテキストに切り替わります。 -
async(context)
: 与えられたコンテキストで新しいコルーチンを開始します。.await()
を呼び出すと、返されたDeferred
タスクを呼び出すと、呼び出したコルーチンを一時停止し、 生成されたコルーチン内で実行されているブロックが戻ると再開されます。
では、次の2つのバージョンの
code
:
バージョン1です。
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
バージョン2です。
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- どちらのバージョンでも、block1(), block3() はデフォルトのコンテキスト(commonpool?)で実行され、block2() は指定されたコンテキストで実行されます。
- 全体の実行は、block1() -> block2() -> block3()の順で同期的に実行されます。
- 唯一の違いは、バージョン1では別のコルーチンが作成され、バージョン2ではコンテキストを切り替えながら1つのコルーチンのみが実行されることです。
私の質問は、:
-
を使用する方が常に良いのではありませんか?
withContext
よりもasync-await
の方が機能的には似ていますが、別のコルーチンを生成しないためです。大量のコルーチンは、軽量ではありますが、要求の厳しいアプリケーションではまだ問題である可能性があります。 -
ケースはありますか
async-await
よりもwithContext
?
更新しました。
Kotlin 1.2.50
を変換するコード検査が追加されました。
async(ctx) { }.await() to withContext(ctx) { }
.
どのように解決するのですか?
<ブロッククオート大量のコルーチンは、軽量とはいえ、要求の厳しいアプリケーションでは問題になる可能性があります。
私は、実際のコストを定量化することによって、「多すぎるコルーチン」は問題であるというこの神話を払拭したいと思います。
まず コルーチン を分離する必要があります。 コルーチンコンテキスト にアタッチします。このようにして、最小限のオーバーヘッドで、ただのコルーチンを作成することができます。
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
この式の値は
Job
であり、中断されたコルーチンを保持しています。継続を保持するために、より広いスコープでリストに追加しています。
このコードをベンチマークしてみたところ、アロケートされた 140バイト であり 100ナノ秒 で完了します。これがコルーチンというものの軽さです。
再現性のために、私が使ったコードはこれです。
fun measureMemoryOfLaunch() {
val continuations = ContinuationList()
val jobs = (1..10_000).mapTo(JobList()) {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
}
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
class JobList : ArrayList<Job>()
class ContinuationList : ArrayList<Continuation<Unit>>()
このコードは多くのコルーチンを起動し、その後スリープするので、VisualVMのような監視ツールでヒープを分析する時間があります。特殊なクラスを作成しました
JobList
と
ContinuationList
とすることで、ヒープダンプの解析が容易になるからです。
より完全な話を得るために、私は以下のコードを使用して、コストも測定し
withContext()
と
async-await
:
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis
const val JOBS_PER_BATCH = 100_000
var blackHoleCount = 0
val threadPool = Executors.newSingleThreadExecutor()!!
val ThreadPool = threadPool.asCoroutineDispatcher()
fun main(args: Array<String>) {
try {
measure("just launch", justLaunch)
measure("launch and withContext", launchAndWithContext)
measure("launch and async", launchAndAsync)
println("Black hole value: $blackHoleCount")
} finally {
threadPool.shutdown()
}
}
fun measure(name: String, block: (Int) -> Job) {
print("Measuring $name, warmup ")
(1..1_000_000).forEach { block(it).cancel() }
println("done.")
System.gc()
System.gc()
val tookOnAverage = (1..20).map { _ ->
System.gc()
System.gc()
var jobs: List<Job> = emptyList()
measureTimeMillis {
jobs = (1..JOBS_PER_BATCH).map(block)
}.also { _ ->
blackHoleCount += jobs.onEach { it.cancel() }.count()
}
}.average()
println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds")
}
fun measureMemory(name:String, block: (Int) -> Job) {
println(name)
val jobs = (1..JOBS_PER_BATCH).map(block)
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
val justLaunch: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {}
}
}
val launchAndWithContext: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
withContext(ThreadPool) {
suspendCoroutine<Unit> {}
}
}
}
val launchAndAsync: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
async(ThreadPool) {
suspendCoroutine<Unit> {}
}.await()
}
}
これは、上記のコードから得られる典型的な出力です。
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
はい。
async-await
の約 2 倍の時間がかかります。
withContext
の約 2 倍の時間がかかりますが、それでもほんの 1 マイクロ秒です。アプリで問題になるには、ループ内で起動し、他にほとんど何もしない必要があります。
使用方法
measureMemory()
呼び出すごとに以下のようなメモリコストが発生することがわかりました。
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
のコストは
async-await
はちょうど 140 バイト高く
withContext
よりもちょうど140バイト多く、これは1つのコルーチンのメモリウェイトとして得た数字です。これは
CommonPool
のコンテキストを設定するコストのほんの一部です。
もしパフォーマンスやメモリへの影響だけが判断基準で
withContext
と
async-await
であるならば、実際のユースケースの 99% において、両者の間に関連する違いはないという結論になるはずです。
本当の理由は
withContext()
の方がよりシンプルで直接的な API であり、特に例外処理の面で優れているからです。
-
で処理されない例外は
async { ... }
内で処理されない例外は、その親ジョブをキャンセルさせます。この現象は、マッチするawait()
. を用意していない場合はcoroutineScope
を用意していないと、アプリケーション全体をダウンさせる可能性があります。 -
で処理されない例外は
withContext { ... }
で処理されない例外は、単にwithContext
の呼び出しによって投げられるだけなので、他のものと同じように処理します。
withContext
も最適化されており、親コルーチンを中断して子コルーチンを待っているという事実を活用していますが、これは単なるおまけです。
async-await
は、実際に並行処理が必要な場合、バックグラウンドで複数のコルーチンを起動し、それに対してのみ待機するような場合にのみ使用すべきです。要するに
-
async-await-async-await
- は使わないでください。withContext-withContext
-
async-async-await-await
- という使い方をします。
関連
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン