1. ホーム
  2. kotlin

[解決済み] Kotlin: withContext() vs Async-await

2022-09-30 12:17:46

疑問点

私はこれまで kotlin ドキュメント を読みましたが、私が正しく理解していれば、2つのKotlinの関数は次のように動作します。

  1. withContext(context) : 現在のコルーチンのコンテキストを切り替え、与えられたブロックが実行されると、コルーチンは以前のコンテキストに切り替わります。
  2. 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()
  }

  1. どちらのバージョンでも、block1(), block3() はデフォルトのコンテキスト(commonpool?)で実行され、block2() は指定されたコンテキストで実行されます。
  2. 全体の実行は、block1() -> block2() -> block3()の順で同期的に実行されます。
  3. 唯一の違いは、バージョン1では別のコルーチンが作成され、バージョン2ではコンテキストを切り替えながら1つのコルーチンのみが実行されることです。

私の質問は、:

  1. を使用する方が常に良いのではありませんか? withContext よりも async-await の方が機能的には似ていますが、別のコルーチンを生成しないためです。大量のコルーチンは、軽量ではありますが、要求の厳しいアプリケーションではまだ問題である可能性があります。

  2. ケースはありますか 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のような監視ツールでヒープを分析する時間があります。特殊なクラスを作成しました JobListContinuationList とすることで、ヒープダンプの解析が容易になるからです。


より完全な話を得るために、私は以下のコードを使用して、コストも測定し 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 のコンテキストを設定するコストのほんの一部です。

もしパフォーマンスやメモリへの影響だけが判断基準で withContextasync-await であるならば、実際のユースケースの 99% において、両者の間に関連する違いはないという結論になるはずです。

本当の理由は withContext() の方がよりシンプルで直接的な API であり、特に例外処理の面で優れているからです。

  • で処理されない例外は async { ... } 内で処理されない例外は、その親ジョブをキャンセルさせます。この現象は、マッチする await() . を用意していない場合は coroutineScope を用意していないと、アプリケーション全体をダウンさせる可能性があります。
  • で処理されない例外は withContext { ... } で処理されない例外は、単に withContext の呼び出しによって投げられるだけなので、他のものと同じように処理します。

withContext も最適化されており、親コルーチンを中断して子コルーチンを待っているという事実を活用していますが、これは単なるおまけです。

async-await は、実際に並行処理が必要な場合、バックグラウンドで複数のコルーチンを起動し、それに対してのみ待機するような場合にのみ使用すべきです。要するに

  • async-await-async-await - は使わないでください。 withContext-withContext
  • async-async-await-await - という使い方をします。