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

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

  • 现在针对以下两个版本的code:
    版本1:
      launch(){
        block1()
        val returned = async(context){
          block2()
        }.await()
        block3()
      }
    
    版本2:
      launch(){
        block1()
         val returned = withContext(context){
          block2()
        }
        block3()
      }
    
  • 在这两个版本中,block1(),block3()在默认context(commonpool?)中执行,而block2()在给定上下文中执行。
  • 总体执行与block1()-> block2()-> block3()顺序同步。
  • 我看到的唯一区别是version1创建了另一个协程,而version2在切换上下文时仅执行一个协程。

  • 我的问题是:
  • 使用withContext而不是async-await并非总是更好,因为它在功能上相似,但是不会创建另一个协程。大量的协程,尽管很轻巧,但在苛刻的应用中仍然可能是一个问题。
  • 是否存在async-awaitwithContext更可取的情况?

  • 更新:
    Kotlin 1.2.50现在具有代码检查功能,可以在其中转换async(ctx) { }.await() to withContext(ctx) { }

    最佳答案



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

    首先,我们应该将协程本身从与之关联的协程上下文中解脱出来。这是您仅以最小的开销创建协程的方式:

    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是我们作为一个协程的内存权重获得的数字。这只是设置withContext上下文的总成本的一小部分。

    如果性能/内存影响是决定async-awaitwithContext()的唯一标准,那么必须得出的结论是,在99%的实际用例中它们之间没有相关的区别。

    真正的原因是async { ... }是一个更简单,更直接的API,尤其是在异常处理方面:
  • await()中未处理的异常导致其父作业被取消。无论您如何处理匹配的coroutineScope的异常,都会发生这种情况。如果您尚未为其准备withContext { ... },则它可能会导致整个应用程序崩溃。
  • withContext调用会抛出在withContext中未处理的异常,您可以像处理其他任何异常一样处理它。
  • async-await恰好也得到了优化,利用了您暂停父协程并等待 child 的事实,但这只是一个额外的好处。
    async-await-async-await应该保留给您实际需要并发的情况,以便您在后台启动多个协程,然后等待它们。简而言之:
  • withContext-withContext —不要这样做,请使用async-async-await-await
  • ojit_code-这就是使用它的方式。
  • 10-08 01:39