Kotlin:withContext()vs Async-await

Man*_*odi 67 kotlin kotlin-coroutines

我一直是kotlin文档,如果我理解正确,两个kotlin函数的工作原理如下:

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

现在为以下两个版本code:

版本1:

  launch(){
    block1()
    val returned = async(context){
      block2()
    }.await()
    block3()
  }
Run Code Online (Sandbox Code Playgroud)

版本2:

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

我的问题是:

  1. 使用它并不总是更好,withContext而不是async-await功能相似,但不会创建另一个协同程序.大型数字协程,虽然轻量级仍然是要求苛刻的应用程序中的问题

  2. 是否有async-await更优先的案例withContext

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

Mar*_*nik 94

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

我想通过量化它们的实际成本来消除这个"太多协程"这个问题的神话.

首先,我们应该将协程本身与它所附带的协程语境区分开来.这就是你如何以最小的开销创建一个协程:

GlobalScope.launch(Dispatchers.Unconfined) {
    suspendCoroutine<Unit> {
        continuations.add(it)
    }
}
Run Code Online (Sandbox Code Playgroud)

这个表达式的值是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>>()
Run Code Online (Sandbox Code Playgroud)

这段代码启动了一堆协同程序然后休眠,因此您有时间使用VisualVM等监视工具分析堆.我创建了专门的类JobList,ContinuationList因为这样可以更容易地分析堆转储.


为了获得更完整的故事,我使用下面的代码来衡量成本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()
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我从上面的代码得到的典型输出:

Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
Run Code Online (Sandbox Code Playgroud)

是的,async-await需要大约两倍的时间withContext,但它仍然只是一微秒.你必须在一个紧凑的循环中启动它们,除此之外什么都不做,因为它会成为你应用中的"问题".

使用measureMemory()我发现每次通话的内存成本如下:

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
Run Code Online (Sandbox Code Playgroud)

成本async-awaitwithContext我们获得的数字高出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 - 这是使用它的方式.

  • 这个答案是宝石!可能是withContext vs async await的最佳解释. (9认同)
  • @IgorGanapolsky 它始终被保留,但通常不会以用户可见的方式保留。失去延续性相当于“Thread.destroy()”——执行消失得无影无踪。 (2认同)

Yog*_*ity 19

如有疑问,请记住以下经验法则:

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

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

  • @IgorGanapolsky如果您正在谈论主线程的阻塞,则“async”和“withContext”不会阻塞主线程,它们只会在某些长时间运行的任务正在运行并等待结果时挂起协程的主体。有关更多信息和示例,请参阅 Medium 上的这篇文章:[使用 Kotlin 协程进行异步操作](https://proandroiddev.com/async-operations-with-kotlin-coroutines-part-1-c51cc581ad33)。 (3认同)

Dmi*_*try 16

使用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)
}
Run Code Online (Sandbox Code Playgroud)

如果您不需要同时运行多个任务,则可以使用withContext.