Kotlin:withContext()与异步等待


91

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

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

现在针对以下两个版本code

版本1:

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

版本2:

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

我的问题是:

  1. 是不是总是更好地使用withContext,而不是async-await因为它的功能类似,但不会创建另一个协程。大量的协程,尽管很轻巧,但在苛刻的应用中仍然可能是一个问题。

  2. 有没有比这async-await更好的情况了withContext

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


我认为withContext,无论何时使用,总是会创建一个新的协程。这是我从源代码中可以看到的。
stdout

async/await根据OP,@ stdout也不会创建新的协程吗?
IgorGanapolsky

Answers:


126

大量的协程,尽管很轻巧,但在苛刻的应用中仍然可能是一个问题

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

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

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

1
@stdout自从我运行这些测试以来,库一直在发展。答案中的代码应该是完全独立的,请尝试再次运行以进行验证。async创建一个Deferred对象,这也可以解释其中的一些区别。
Marko Topolnik,

〜“保留延续”。我们什么时候需要保留呢?
IgorGanapolsky

1
@IgorGanapolsky它始终保留,但通常对用户不可见。失去延续性等同于Thread.destroy()-执行力消失thin尽。
Marko Topolnik

22

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

是否有案例asynch-wait比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。


13

如有疑问,请牢记以下规则:

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

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


1
两者async和都withContext处于暂停范围内吗?
IgorGanapolsky

3
@IgorGanapolsky如果您正在谈论阻塞主线程,asyncwithContext不会阻塞主线程,则它们只会在一些长时间运行的任务正在运行并等待结果时挂起协程的主体。有关更多信息和示例,请参见“中等:使用Kotlin Coroutines进行异步操作”上的这篇文章。
Yogesh Umesh Vaity
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.