Kotlin: withContext() vs Async-await

前端 未结 3 1047
栀梦
栀梦 2020-12-22 17:00

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

  1. withContext(context): switches the contex
相关标签:
3条回答
  • 2020-12-22 17:10

    When in doubt, remember this like a rule of thumb:

    1. If multiple tasks have to happen in parallel and final result depends on completion of all of them, then use async.

    2. For returning the result of a single task, use withContext.

    0 讨论(0)
  • 2020-12-22 17:15

    Large number of coroutines, though lightweight, could still be a problem in demanding applications

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

    The value of this expression is a Job holding a suspended coroutine. To retain the continuation, we added it to a list in the wider scope.

    I benchmarked this code and concluded that it allocates 140 bytes and takes 100 nanoseconds to complete. So that's how lightweight a coroutine is.

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

    This is the typical output I get from the above code:

    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.

    Using measureMemory() I found the following memory cost per call:

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

    The cost of async-await is exactly 140 bytes higher than withContext, the number we got as the memory weight of one coroutine. This is just a fraction of the complete cost of setting up the CommonPool context.

    If performance/memory impact was the only criterion to decide between withContext and async-await, the conclusion would have to be that there's no relevant difference between them in 99% of real use cases.

    The real reason is that withContext() a simpler and more direct API, especially in terms of exception handling:

    • 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 also happens to be optimized, leveraging the fact that you're suspending the parent coroutine and awaiting on the child, but that's just an added bonus.

    async-await should be reserved for those cases where you actually want concurrency, so that you launch several coroutines in the background and only then await on them. In short:

    • async-await-async-await — don't do that, use withContext-withContext
    • async-async-await-await — that's the way to use it.
    0 讨论(0)
  • 2020-12-22 17:26

    Isn't it always better to use withContext rather than asynch-await as it is funcationally similar, but doesn't create another coroutine. Large numebrs coroutines, though lightweight could still be a problem in demanding applications

    Is there a case asynch-await is more preferable to withContext

    You should use async/await when you want to execute multiple tasks concurrently, for example:

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

    If you don't need to run multiple tasks concurrently you can use withContext.

    0 讨论(0)
提交回复
热议问题