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

查看:2505
本文介绍了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(),它将暂停调用协程,并在派生的协程内部执行的块返回时恢复. li>
  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()在默认上下文(commonpool?)中执行,而block()在给定上下文中执行.
  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?

更新: 科林1.2.50 现在有可以转换async(ctx) { }.await() to 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.

首先,我们应将协程自身与附加的协程上下文分开.这是您仅以最小的开销创建协程的方式:

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天全站免登陆