Muhammad Talha
05/12/2022, 1:00 AMsuspend fun getData(): Data = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
val a = async(<http://Dispatchers.IO|Dispatchers.IO>) {
delay(1000)
Data("some data...")
}
a.await()
}
streetsofboston
05/12/2022, 1:51 AMwithContext(...) {
, write coroutineScope {
.Alexandre Brown
05/12/2022, 3:28 AMwithContext(myDispatcher){ }
will ensure that your coroutines inside the { }
are in the myDispatcher
whereas coroutineScope
will use whatever the parent context is (eg: context defined by caller perhaps). This could result in unwanted behaviours if that's not what you want.
For instance, if caller used Dispatchers.Main or Dispatchers.Default but you are doing IO then it'd be better to explicitly state that your code block should run in Dispatchers.IO using withContextMuhammad Talha
05/12/2022, 3:39 AMasync
in either case to make sure it's a new coroutine?streetsofboston
05/12/2022, 11:50 AMAlexandre Brown
05/12/2022, 12:18 PMwithContext
instead of specifying <http://Dispatchers.IO|Dispatchers.IO>
in all the lauch
and async
within a same block of code that I know will run on the same dispatcher.
Usually I will pass the coroutine scope as constructor parameter to allow dependency injection to provide it and then during test I can also use a TestScope
from runTest
.
class MyImplementation(
private val coroutineScope: CoroutineScope
) : MyInterface, CoroutineScope by coroutineScope {
suspend fun myFun() =
withContext(coroutineContext) {
Note that doing async + await
is equivalent to doing withContext
since withContext
will start your block of code in a new coroutine and suspend until it completes.
The IDE will even suggest you to use withContext
instead if you have async + await
right away.
One last thing, coroutineScope
has a different behaviour where if a child coroutine fails, all will be cancelled. This might or might not be what you want.
From the doc :
coroutineScope
Creates a CoroutineScope and calls the specified suspend block with this scope.
The provided scope inherits its coroutineContext from the outer scope, but overrides the context's Job.
This function is designed for parallel decomposition of work. When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled (for a different behavior see supervisorScope).
streetsofboston
05/12/2022, 12:31 PMcoroutineScope
since the call to async
requires a CoroutineScope instance as its receiver.Alexandre Brown
05/12/2022, 12:36 PMwithContext
to get concurrent executions.streetsofboston
05/12/2022, 12:40 PMMuhammad Talha
05/12/2022, 10:38 PMAlexandre Brown
05/12/2022, 10:46 PMwithContext
and coroutineScope
both have this in their impl:
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
But that's just normal and if the dispatcher is the same then there wont be any new thread created and no context switching.
So the main differences is the coroutine context inheritance (coroutineScope inherits the parent while withContext specifies it explicitly) and child cancellation behaviour as mentionned.Muhammad Talha
05/12/2022, 10:53 PMAlexandre Brown
05/12/2022, 11:17 PMwithContext
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
val newContext = oldContext + context
// always check for cancellation of new context
newContext.ensureActive()
// FAST PATH #1 -- new context is the same as the old one
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
// There are changes in the context, so this thread needs to be updated
withCoroutineContext(newContext, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
// SLOW PATH -- use new dispatcher
val coroutine = DispatchedCoroutine(newContext, uCont)
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}
Here is the implementation of coroutineScope
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
You can see that if new context == old context, then withContext
is pretty much like coroutineScope
Btw there are shared threads between <http://Dispatchers.IO|Dispatchers.IO>
and Dispatchers.Default
to avoid context switching when possible even when context are different