Kotlin: withContext() vs Async-waiting

我一直在阅读 kotlin docs,如果我理解正确的话,两个 Kotlin 函数的工作原理如下:

  1. withContext(context): 切换当前协程的上下文,当给定块执行时,协程切换回以前的上下文。
  2. async(context): 在给定的上下文中启动一个新的协程,如果我们对返回的 Deferred任务调用 .await(),它将挂起调用协程,并在衍生协程中执行的块返回时恢复。

下面是 code的两个版本:

第一版:

  launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}

第二版:

  launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
  1. 在两个版本的 lock1()中,lock3()都是在默认上下文(commonpool?)中执行的,其中 lock2()在给定的上下文中执行。
  2. 总体执行与 lock1()-> lock2()-> lock3()顺序同步。
  3. 我看到的唯一区别是 version 1创建了另一个协程,其中 version 2在切换上下文时只执行一个协程。

我的问题是:

  1. Isn't it always better to use withContext rather than async-await as it is functionally similar, but doesn't create another coroutine. Large numbers of coroutines, although lightweight, could still be a problem in demanding applications.

  2. Is there a case async-await is more preferable to withContext?

更新: Kotlin 1.2.50 now has a code inspection where it can convert async(ctx) { }.await() to withContext(ctx) { }.

36013 次浏览

与异步相比,使用 withContext 不是总是更好吗? ——等待,因为它在功能上是相似的,但不会创建另一个协程。大量的协同程序,虽然轻量级仍然是一个需要应用程序的问题

是否存在异步-等待比 withContext 更可取的情况

当需要并发执行多个任务时,应该使用异步/等待,例如:

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() }
//Or val results = deferredResults.awaitAll()
println(results)
}

如果不需要同时运行多个任务,可以使用 withContext。

大量的协同程序,虽然是轻量级的,但是在需要的应用程序中仍然是一个问题

我想通过量化协同程序的实际成本来消除“太多协同程序”是一个问题的神话。

首先,我们应该将 跳绳本身从它所连接的 协同程序上下文中分离出来。下面是如何创建一个开销最小的协同程序:

GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}

The value of this expression is a Job holding a suspended coroutine. To retain the continuation, we added it to a list in the wider scope.

我对这段代码进行了基准测试,得出的结论是,它分配 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>>()

This code starts a bunch of coroutines and then sleeps so you have time to analyze the heap with a monitoring tool like VisualVM. I created the specialized classes JobList and ContinuationList because this makes it easier to analyze the heap dump.


To get a more complete story, I used the code below to also measure the cost of withContext() and 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的两倍,但它仍然只有一微秒。你必须在一个紧密的循环中启动它们,除此之外几乎什么也不做,这样才会成为你的应用程序中的“问题”。

通过使用 measureMemory(),我发现每次呼叫的内存成本如下:

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

async-await的开销比 withContext整整高出140个字节,这个数字是我们得到的一个协同程序的内存重量。这只是设置 CommonPool上下文的完全成本的一小部分。

如果性能/内存影响是决定 withContextasync-await之间的唯一标准,那么结论就是在99% 的实际用例中它们之间没有相关的差异。

真正的原因是 withContext()是一个更简单、更直接的 API,特别是在异常处理方面:

  • async { ... }中未处理的异常会导致其父作业被取消。无论您如何处理来自匹配 await()的异常,都会发生这种情况。如果您还没有为它准备一个 coroutineScope,它可能会使您的整个应用程序崩溃。
  • withContext { ... }中没有处理的异常只是由 withContext调用抛出,您可以像处理其他异常一样处理它。

withContext也碰巧进行了优化,利用了暂停父协程并等待子协程的事实,但这只是一个额外的好处。

async-await应该保留给那些实际需要并发的情况,这样就可以在后台启动几个协程,然后等待它们。简而言之:

  • 不要这样做,使用 withContext-withContext
  • 这就是使用它的方法。

当有疑问的时候,记住一条经验法则:

  1. If multiple tasks have to happen in parallel and the final result depends on completion of all of them, then use async.

  2. 若要返回单个任务的结果,请使用 withContext