【问题标题】:Kotlin: withContext() vs Async-awaitKotlin:withContext() 与 Async-await
【发布时间】:2018-10-18 05:16:24
【问题描述】:

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

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

下面两个版本的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. 我看到的唯一区别是 version1 创建了另一个协程,而 version2 在切换上下文时只执行一个协程。

我的问题是:

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

  2. 有没有async-awaitwithContext更可取的情况?

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

【问题讨论】:

  • 我认为当你使用withContext 时,无论如何都会创建一个新的协程。这是我从源代码中看到的。
  • @stdout 根据 OP,async/await 不也创建一个新的协程吗?

标签: kotlin kotlin-coroutines


【解决方案1】:

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

我想通过量化它们的实际成本来消除“太多协程”成为问题的神话。

首先,我们应该将 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 等监控工具分析堆。我创建了专门的类JobListContinuationList,因为这样可以更轻松地分析堆转储。


为了获得更完整的故事,我使用下面的代码还测量了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 的两倍,但它仍然只是一微秒。您必须在一个紧密的循环中启动它们,除此之外几乎什么都不做,这会成为您应用中的“问题”。

使用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 应该保留给那些你真正需要并发的情况,这样你就可以在后台启动几个协程,然后才等待它们。简而言之:

  • async-await-async-await — 不要那样做,使用 withContext-withContext
  • async-async-await-await — 这就是使用它的方式。

【讨论】:

  • 关于async-await的额外内存开销:当我们使用withContext时,也会创建一个新的协程(从源代码中我可以看到)所以你认为差异可能是从其他地方来的吗?
  • @stdout 自从我运行这些测试以来,库一直在发展。答案中的代码应该是完全独立的,请尝试再次运行以进行验证。 async 创建一个 Deferred 对象,这也可以解释一些差异。
  • ~"保留续行"。我们什么时候需要保留这个?
  • @IgorGanapolsky 它始终被保留,但通常不以用户可见的方式。失去延续等同于Thread.destroy() -- 执行化为乌有。
【解决方案2】:

使用 withContext 而不是 asynch-await 不是总是更好吗,因为它在功能上相似,但不会创建另一个协程。大数字协程,尽管轻量级在要求苛刻的应用程序中仍然可能是一个问题

是否有一种情况 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。

【讨论】:

    【解决方案3】:

    如有疑问,请记住这一点:

    1. 如果多个任务必须并行执行且最终结果取决于所有任务的完成,则使用async

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

    【讨论】:

    • asyncwithContext 是否都在挂起范围内阻塞?
    • @IgorGanapolsky 如果你在谈论阻塞主线程,asyncwithContext 不会阻塞主线程,它们只会暂停协程的主体,而一些长时间运行的任务是运行并等待结果。有关更多信息和示例,请参阅 Medium 上的这篇文章:Async Operations with Kotlin Coroutines
    猜你喜欢
    • 2021-11-13
    • 1970-01-01
    • 2021-06-15
    • 1970-01-01
    • 1970-01-01
    • 2019-09-06
    • 1970-01-01
    • 2015-04-30
    • 2020-02-05
    相关资源
    最近更新 更多