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

  1. withContext(context): switches the context of the current coroutine, when the given block executes, the coroutine switches back to previous context.
  2. 在给定的上下文中启动一个新的协程,如果我们在返回的第Deferred个任务上调用.await()时,它将暂停调用协程,并在生成的协程内执行的挡路返回时继续执行.

现在,请看以下两个版本的code:

Version1:

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

Version2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. In both versions block1(), block3() execute in default context(commonpool?) where as block2() executes in the given context.
  2. 整体执行与Block1()->Block2()->Block3()顺序同步.
  3. 我看到的唯一区别是版本1创建了另一个协同路由,其中as版本2在切换上下文时只执行一个协同路由.

My questions are :

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

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

Update: Kotlin 1.2.50现在有一个代码判断,它可以在其中转换async(ctx) { }.await() to withContext(ctx) { }.

推荐答案

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

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,表示挂起的协程.为了保持连续性,我们将其添加到范围更广的列表中.

我对这段代码进行了基准测试,得出的结论是它分配了140 bytes个资源,需要100 nanoseconds个时间才能完成.这就是协程的轻量级.

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

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

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(),我发现每次通话的内存成本如下:

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

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

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

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

  • 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也恰巧得到了优化,利用了暂停父协同进程并等待子进程的事实,但这只是一个额外的好处.

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

  • async-await-async-await — don't do that, use withContext-withContext
  • async-async-await-await — that's the way to use it.

Kotlin相关问答推荐

只能在元素区域中点击的Jetpack Compose列

Kotlin中的增广赋值语句中的难以理解的错误

如何检测一个值是否是Kotlin中的枚举实例?

Kotlin 函数名后面的大括号是什么意思?

Kotlin RxJava 可空的错误

比较 Kotlin 中的可比对象列表

TextField maxLength - Android Jetpack Compose

如何在Spring Boot应用程序上启用承载身份验证?

Android:在 DAO 中使用 Room 数据库和 LiveData 架构

如何将 Kotlin 日期中的字符串或时间戳格式化为指定的首选格式?

Spring Boot:更改属性占位符符号

如何获取Kotlin中变量的名称?

Android EditText 协程go 抖操作符,如 RxJava

IllegalStateException:function = , count = 3, index = 3

如何限制kotlin协程的最大并发性

Kotlin flatMap - map

内联 Kotlin 方法没有覆盖报告

Kotlin Flow 收集后无法执行代码

Kotlin反射不可用

如何在 kotlin 中创建重复对象数组?