Kotlin:withContext()とAsync-await


91

私は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) { }


を使用するwithContextと、関係なく常に新しいコルーチンが作成されると思います。これは私がソースコードから見ることができるものです。
標準出力

@stdout async/awaitOPによると、新しいコルーチンも作成しませんか?
IgorGanapolsky

回答:


126

多数のコルーチンは軽量ですが、要求の厳しいアプリケーションでは依然として問題になる可能性があります

「コルーチンが多すぎる」というこの神話を、実際のコストを数値化することで解消したいと思います。

まず、コルーチン自体を、それがアタッチされているコルーチンコンテキストから解きほぐす必要があります。これは、最小限のオーバーヘッドでコルーチンを作成する方法です。

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が、それでも1マイクロ秒です。それがアプリの「問題」になるためには、タイトなループでそれらを起動する必要があり、それ以外はほとんど何もしません。

を使用しmeasureMemory()て、呼び出しごとに次のメモリコストが見つかりました。

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes

のコストは、1つのコルーチンのメモリの重みとして取得した数async-awaitよりも正確に140バイト高くなりますwithContext。これは、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 —それがそれを使用する方法です。

の追加メモリコストについてasync-await:を使用するwithContextと、新しいコルーチンも作成されます(ソースコードからわかる限り)。違いはどこかから来ているのではないかと思いますか?
標準出力

1
@stdoutこれらのテストを実行してから、ライブラリは進化してきました。回答のコードは完全に自己完結型であると想定されています。検証するためにもう一度実行してみてください。オブジェクトをasync作成しDeferredます。これは、いくつかの違いを説明する場合もあります。
MarkoTopolnik19年

〜「継続を保持する」。これをいつ保持する必要がありますか?
IgorGanapolsky

1
@IgorGanapolsky常に保持されますが、通常はユーザーに表示されません。継続を失うことは、Thread.destroy()実行が薄い空気に消えていくのと同じです。
Marko Topolnik

22

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

withContextよりもasynch-awaitの方が望ましい場合はありますか

たとえば、複数のタスクを同時に実行する場合は、async / awaitを使用する必要があります。

runBlocking {
    val deferredResults = arrayListOf<Deferred<String>>()

    deferredResults += async {
        delay(1, TimeUnit.SECONDS)
        "1"
    }

    deferredResults += async {
        delay(1, TimeUnit.SECONDS)
        "2"
    }

    deferredResults += async {
        delay(1, TimeUnit.SECONDS)
        "3"
    }

    //wait for all results (at this point tasks are running)
    val results = deferredResults.map { it.await() }
    println(results)
}

複数のタスクを同時に実行する必要がない場合は、withContextを使用できます。


13

疑わしい場合は、経験則のようにこれを覚えておいてください。

  1. 複数のタスクを並行して実行する必要があり、最終結果がそれらすべての完了に依存する場合は、を使用しますasync

  2. 単一のタスクの結果を返すには、を使用しますwithContext


1
両方あるasyncwithContext、サスペンド範囲でブロックしますか?
IgorGanapolsky

3
@IgorGanapolskyあなたは、メインスレッドのブロッキングについて話している場合、asyncおよびwithContext一部の長期実行中のタスクが実行され、結果を待っている間、メインスレッドをブロックすることはありません、彼らは唯一のコルーチンの本体を一時停止します。詳細と例については、Medium:Kotlinコルーチンを使用した非同期操作に関するこの記事を参照してください。
Yogesh Umesh Vaity
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.