kotlin docs であり、正しく理解すれば、2つのkotlin関数は次のように機能します。
withContext(context)
:現在のコルーチンのコンテキストを切り替えます。指定されたブロックが実行されると、コルーチンは前のコンテキストに戻ります。async(context)
:指定されたコンテキストで新しいコルーチンを開始し、返されたDeferred
タスクで.await()
を呼び出すと、呼び出したコルーチンを一時停止し、生成されたコルーチン内で実行中のブロックが戻ると再開します。code
の次の2つのバージョンの場合:
バージョン1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
バージョン2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
私の質問は:
機能的には似ていますが、別のコルーチンを作成しないため、async-await
よりもwithContext
を使用する方が常に良いとは限りません。大量のコルーチンは、軽量ではありますが、要求の厳しいアプリケーションでは依然として問題になる可能性があります。
async-await
がwithContext
よりも望ましい場合がありますか?
更新:Kotlin 1.2.5 には、async(ctx) { }.await() to withContext(ctx) { }
を変換できるコード検査があります。
多数のコルーチンは、軽量ではありますが、要求の厳しいアプリケーションでは依然として問題になる可能性があります
実際のコストを定量化することで、問題である「多すぎるコルーチン」というこの神話を払拭したいと思います。
まず、coroutine自体をcoroutine contextから解きほぐす必要があります添付されています。これは、最小限のオーバーヘッドでコルーチンを作成する方法です。
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
はwithContext
の約2倍の時間がかかりますが、それでもほんの1マイクロ秒です。アプリで「問題」になるためには、それらをタイトなループで起動し、それ以外はほとんど何もしません。
measureMemory()
を使用すると、呼び出しごとに次のメモリコストが見つかりました。
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
async-await
のコストは、1つのコルーチンのメモリウェイトとして得られたwithContext
よりも正確に140バイト高くなります。これは、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
—それが使用方法です。機能的には似ていますが、別のコルーチンを作成しないため、asynch-awaitよりもwithContextを使用する方が常に良いとは限りません。大きい数値のコルーチン。ただし、要求の厳しいアプリケーションでは軽量が問題になる可能性があります。
Asynch-awaitがwithContextよりも望ましい場合がありますか
複数のタスクを同時に実行する場合は、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を使用できます。