I have been reading kotlin docs, and if I understood correctly the two Kotlin functions work as follows :
withContext(context)
: switches the context of the current coroutine, when the given block executes, the coroutine switches back to previous context.async(context)
: Starts a new coroutine in the given context and if we call .await()
on the returned Deferred
task, it will suspends the calling coroutine and resume when the block executing inside the spawned coroutine returns.Now for the following two versions of code
:
Version1:
launch(){ block1() val returned = async(context){ block2() }.await() block3() }
Version2:
launch(){ block1() val returned = withContext(context){ block2() } block3() }
My questions are :
Isn't it always better to use withContext
rather than async-await
as it is functionally similar, but doesn't create another coroutine. Large numbers of coroutines, although lightweight, could still be a problem in demanding applications.
Is there a case async-await
is more preferable to withContext
?
Update: Kotlin 1.2.50 now has a code inspection where it can convert async(ctx) { }.await() to withContext(ctx) { }
.
withContext(context) : switches the context of the current coroutine, when the given block executes, the coroutine switches back to previous context. async(context) : Starts a new coroutine in the given context and if we call .
A good practice is to use withContext() to make sure every function is main-safe, which means that you can call the function from the main thread. This way, the caller never needs to think about which thread should be used to execute the function.
In order to migrate to the async/await pattern, you have to return the async() result from your code, and call await() on the Deferred , from within another coroutine. By doing so, you can remove callbacks you used to use, to consume asynchronously provided values.
withContext is a scope function that allows us to create a new cancelable coroutine. If we pass a CoroutineContext arg, withContext merges the parent context and our arg to create a new CoroutineContext, then executes the coroutine within this merged context.
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:
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.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.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With