Kotlin:withContext() 与异步等待 [英] Kotlin: withContext() vs Async-await

查看:24
本文介绍了Kotlin:withContext() 与异步等待的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在阅读 kotlin 文档,并且如果我理解正确,这两个 Kotlin 函数的工作方式如下:

I have been reading kotlin docs, and if I understood correctly the two Kotlin functions work as follows :

  1. withContext(context):切换当前协程的上下文,当给定块执行时,协程切换回之前的上下文.
  2. async(context):在给定的上下文中启动一个新的协程,如果我们在返回的 Deferred 任务上调用 .await(),它将挂起调用协程,并在生成的协程内执行的块返回时恢复.
  1. withContext(context): switches the context of the current coroutine, when the given block executes, the coroutine switches back to previous context.
  2. async(context): Starts a new coroutine in the given context and if we call .await() on the returned Deferred task, it will suspends the calling coroutine and resume when the block executing inside the spawned coroutine returns.

现在对于code的以下两个版本:

Now for the following two versions of code :

版本 1:

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

版本 2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }

  1. 在这两个版本中,block1()、block3() 在默认上下文(公共池?)中执行,而 block2() 在给定上下文中执行.
  2. 整体执行与block1()同步->block2() ->block3() 顺序.
  3. 我看到的唯一区别是 version1 创建了另一个协程,而 version2 在切换上下文时只执行一个协程.

我的问题是:

  1. 使用 withContext 而不是 async-await 是不是总是更好,因为它在功能上相似,但不会创建另一个协程.大量的协程虽然轻量级,但在要求苛刻的应用程序中仍然是一个问题.

  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.

是否有一种情况 async-awaitwithContext 更可取?

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

更新:Kotlin 1.2.50 现在有代码检查,它可以将 async(ctx) { }.await() 转换为 withContext(ctx) { }.

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

推荐答案

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

Large number of coroutines, though lightweight, could still be a problem in demanding applications

我想通过量化它们的实际成本来消除协程太多"是一个问题的神话.

I'd like to dispel this myth of "too many coroutines" being a problem by quantifying their actual cost.

首先,我们应该将 coroutine 本身与它所附加的 coroutine 上下文 分开.这就是你如何以最小的开销创建一个协程:

First, we should disentangle the coroutine itself from the coroutine context to which it is attached. This is how you create just a coroutine with minimum overhead:

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

这个表达式的值是一个 Job 持有一个暂停的协程.为了保留延续性,我们将其添加到范围更广的列表中.

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 纳秒来完成.这就是协程的轻量级.

I benchmarked this code and concluded that it allocates 140 bytes and takes 100 nanoseconds to complete. So that's how lightweight a coroutine is.

为了重现性,这是我使用的代码:

For reproducibility, this is the code I used:

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,因为这样可以更轻松地分析堆转储.

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.

为了获得更完整的故事,我还使用了下面的代码来衡量 withContext()async-await 的成本:

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()
    }
}

这是我从上面的代码中得到的典型输出:

This is the typical output I get from the above code:

Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds

是的,async-await 的时间大约是 withContext 的两倍,但它仍然只是一微秒.您必须在一个紧密的循环中启动它们,除此之外几乎什么都不做,否则这会成为您应用中的问题".

Yes, async-await takes about twice as long as withContext, but it's still just a microsecond. You'd have to launch them in a tight loop, doing almost nothing besides, for that to become "a problem" in your app.

使用 measureMemory() 我发现每次调用的内存成本如下:

Using measureMemory() I found the following memory cost per call:

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

async-await 的成本正好比 withContext 高 140 个字节,我们得到的数字是一个协程的内存权重.这只是设置 CommonPool 上下文的全部成本的一小部分.

The cost of async-await is exactly 140 bytes higher than withContext, the number we got as the memory weight of one coroutine. This is just a fraction of the complete cost of setting up the CommonPool context.

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

If performance/memory impact was the only criterion to decide between withContext and async-await, the conclusion would have to be that there's no relevant difference between them in 99% of real use cases.

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

The real reason is that withContext() a simpler and more direct API, especially in terms of exception handling:

  • 未在 async { ... } 中处理的异常会导致其父作业被取消.无论您如何处理来自匹配 await() 的异常,都会发生这种情况.如果您还没有为它准备 coroutineScope,它可能会导致您的整个应用程序崩溃.
  • 未在 withContext { ... } 中处理的异常会被 withContext 调用抛出,您可以像处理其他任何事情一样处理它.
  • An exception that isn't handled within async { ... } causes its parent job to get cancelled. This happens regardless of how you handle exceptions from the matching await(). If you haven't prepared a coroutineScope for it, it may bring down your entire application.
  • An exception not handled within withContext { ... } simply gets thrown by the withContext call, you handle it just like any other.

withContext 也恰好经过优化,利用了暂停父协程并等待子协程的事实,但这只是一个额外的好处.

withContext also happens to be optimized, leveraging the fact that you're suspending the parent coroutine and awaiting on the child, but that's just an added bonus.

async-await 应该保留用于那些您确实需要并发的情况,以便您在后台启动多个协程,然后才等待它们.简而言之:

async-await should be reserved for those cases where you actually want concurrency, so that you launch several coroutines in the background and only then await on them. In short:

  • async-await-async-await —不要那样做,使用 withContext-withContext
  • async-async-await-await —这就是使用它的方式.
  • async-await-async-await — don't do that, use withContext-withContext
  • async-async-await-await — that's the way to use it.

这篇关于Kotlin:withContext() 与异步等待的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆