Carrascado
08/23/2020, 4:06 PMlaunch {}
continues being executed instantly (although because structured concurrency, coroutine will finally wait for its children to finish).
• async: Creates a new coroutine. Code below async {}
continues being executed instantly (although because structured concurrency, coroutine will finally wait for its children to finish).
• runBlocking: Creates a new coroutine. Execution is blocked. Code below runBlocking {}
won't execute until runBlocking has ended.
• coroutineScope: Creates a new coroutine. Execution is suspended (it's the suspending version of runBlocking). Code below coroutineScope {}
won't execute until coroutineScope has ended.
Now, if this is correct, the only coroutine builder whose name makes sense is runBlocking 🤷♂️ The other ones seem to have absolutely random names. The 4 coroutine builders "launch" the coroutine, two of them are "asynchronous" in some sense (both launch and async), and 4 of 4 again have a "coroutine scope", what am I not understanding here? Why do they have these names?Zach Klippenstein (he/him) [MOD]
08/23/2020, 4:18 PMasync
is the most surprising of these names, but I think it’s called that because it’s supposed to be one-half of the `async`/`await` keywords in other languages (javascript, python, c#). It’s probably named this way for discoverability and to hint at how it’s meant to be used for people familiar with async/await. But if you don’t have that context, the name does seem super random.launch
is the most basic way to “launch” a coroutine. It just launches it, fire-and-forget, it doesn’t do anything else. async
could probably be called launchForValue
if it wasn’t for the async/await thing.Carrascado
08/23/2020, 4:23 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:24 PMcoroutineScope
doesn’t really create a new coroutine, really, anymore than runBlocking
creates a new thread. It simply delineates a scope within which new coroutines can be created. Whereas runBlocking
is usually used in a way where it will run a bunch of coroutines for some time and block the thread for potentially quite a long time (the “blocking” nature of the call is very important), coroutineScope
is primarily meant to define a scope. The scoping is the more important thing, not the suspending.coroutineScope
is used when you need to get a scope in which to start some coroutines from inside a suspend function. runBlocking
is used in the more rare case when you’re not already in the coroutine world and need to block the current thread in order to run some coroutines.Carrascado
08/23/2020, 4:27 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:28 PMCarrascado
08/23/2020, 4:28 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:29 PMrunBlocking
) inherits scope.coroutineScope {}
is basically the same thing as writing with(CoroutineScope(coroutineContext + Job(parent = coroutineContext[Job]))) {}
Carrascado
08/23/2020, 4:31 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:32 PMlaunch
and async
create new jobs, yes. I don’t really think of them as “adding the job to the scope” though, since they aren’t mutating the current scope. They’re creating a new scopes which shares everything from the current scope, except the job is a child of the current job.Carrascado
08/23/2020, 4:34 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:35 PMcoroutineScope
is to create a “group” of child coroutines so that the parent coroutine can wait for them all to be completed, and that will all be cancelled if the parent is.Carrascado
08/23/2020, 4:39 PMcoroutineScope
, specially when launch and async don't make you wait but they create new "coroutineScopes" too 😕Zach Klippenstein (he/him) [MOD]
08/23/2020, 4:40 PMsuspend fun doSomeThings() {
launch { doFirstThing() }
launch { doSecondThing() }
}
But that won’t work, because launch
is defined as an extension function on CoroutineScope
. And if it did work, e.g. you used the GlobalScope
, then these coroutines would be leaked – if they never finished, then they would take up memory/resources forever. And there’s no way to cancel them. Instead you’d write:
suspend fun doSomeThings() {
coroutineScope {
launch { doFirstThing() }
launch { doSecondThing() }
}
}
coroutineScope
just gives you a block which lets you start coroutines. You can’t even call launch/async without a scope, and coroutineScope
is a good way to get one if you’re already in a suspend function.Carrascado
08/23/2020, 4:41 PMcoroutineScope
know who is the parent scope? Is it the parent scope of the suspending function caller?Zach Klippenstein (he/him) [MOD]
08/23/2020, 4:43 PMdoSomeThings()
is the parent of the job inside coroutineScope
, which is the parent of the jobs that doFirstThing()
and doSecondThing()
see.Carrascado
08/23/2020, 4:44 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:45 PMCarrascado
08/23/2020, 4:46 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:47 PMCoroutineScope
instance, but the most common and simplest way is to use coroutineScope{}
. But you can, for example, write
val scope = CoroutineScope(<http://Dispatchers.IO|Dispatchers.IO>)
scope.launch {}
However in that case, the scope’s job is not a child of the current job, so you’ve just broken structured concurrency. Don’t do that unless you really mean to.Carrascado
08/23/2020, 4:52 PMwithContext(<http://Dispatchers.IO|Dispatchers.IO>)
break it too? Why is it recommended practice?withContext
is called like that and not withDispatcher
?"Zach Klippenstein (he/him) [MOD]
08/23/2020, 4:53 PMHowever in that case, the scope’s job is not a child of the current job, so you’ve just broken structured concurrency.Yes it does 🙂
withContext
can change any part of the context, not just the dispatcher. E.g. you can do withContext(CoroutineName("debug me"))
Carrascado
08/23/2020, 4:54 PMZach Klippenstein (he/him) [MOD]
08/23/2020, 4:54 PMwithContext
doesn’t break structured concurrency unless you pass in a job without a parent.withContext(<http://Dispatchers.IO|Dispatchers.IO>)
is fine, but withContext(Job())
would break it (actually not sure if that would even run, it might throw an exception if you try to pass a different job in).CoroutineScope(Job(parent = coroutineContext[Job]))
, then it would not break structured concurrency.Map
. withContext(foo) {}
is basically just with(coroutineContext + foo) {}
, where coroutineContext
is the context of the caller.Carrascado
08/23/2020, 4:59 PMwithContext(<http://Dispatchers.IO|Dispatchers.IO>)
, it creates a new context with the new dispatcher but it defaults all the other stuff with whatever it was before?Zach Klippenstein (he/him) [MOD]
08/23/2020, 5:01 PMSo the job hierarchy is what really “defines” the structured concurrency?YES! Exactly!
if Context has 5 properties, when you call withContext(Dispatchers.IO), it creates a new context with the new dispatcher but it defaults all the other stuff with whatever it was before?Also yes!
Carrascado
08/23/2020, 5:02 PMjulian
08/24/2020, 3:28 PMtaer
08/26/2020, 3:21 AMtest()
which HAS to be a couroutine(since it is suspendable) get to become the parent of the coroutineScope
defined in test
?
from the doc:
The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* the context's [Job].
Is it just part of the suspendCoroutineUninterceptedOrReturn
implementation that links the two?coroutineContext
seems to be a local variable inside of suspend functions.
@InlineOnly
public suspend inline val coroutineContext: CoroutineContext
get() {
throw NotImplementedError("Implemented as intrinsic")
}
What is that?Zach Klippenstein (he/him) [MOD]
08/26/2020, 4:25 PMold threadOnly two days 😛
Is it just part of the suspendCoroutineUninterceptedOrReturn implementation that links the two?Not quite.
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
The current context (uCont.context
) is passed to the ScopeCoroutine
constructor, which passes it through to the AbstractCoroutine
constructor. AbstractCoroutine
wires up the job inside the context to be the parent of itself (see initParentJobInternal()
). AbstractCoroutine
also implements CoroutineScope
, and so the coroutine
is passed as the receiver for the coroutine block (i.e. the first arg to startUndispatchedOrReturn()
).Carrascado
08/26/2020, 4:29 PMcoroutineScope
as a parent?Zach Klippenstein (he/him) [MOD]
08/26/2020, 4:30 PMcoroutineContext seems to be a local variable inside of suspend functions.It’s not a local variable, it’s a top-level/global property. It’s basically shorthand for writing:
val coroutineContext = suspendCoroutine { continuation ->
continuation.resume(continuation.context)
}
That is, the continuations that are passed around as the core currency of coroutines always contains the current context, which you can see by calling one of the suspendCoroutine
functions to actually get access to a continuation. coroutineContext
is just a convenient way to get it without the boilerplate.CoroutineScope
is nothing special. It’s just a thing that points to a CoroutineContext
.Carrascado
08/26/2020, 4:32 PMZach Klippenstein (he/him) [MOD]
08/26/2020, 4:33 PMCoroutineScope
is a thing at all is to serve as a place to hang extension methods off of. It’s a common pattern for functions that take lambdas-with-receivers to call the receiver type SomethingScope
. But as far as the actual semantics are concerned for coroutines, “scope” and “context” are the same thing (a scope is just a different way of referring to a context).CoroutineScope
is literally just
interface CoroutineScope {
val coroutineContext: CoroutineContext
}
interface CoroutineScope {
val coroutineContext: Map<ContextKey, Any?>
}
Carrascado
08/26/2020, 4:36 PMZach Klippenstein (he/him) [MOD]
08/26/2020, 4:40 PMCoroutineContext
is the core data type that actually matters, it came before CoroutineScope
, it actually holds the context, etc. And the two are effectively interchangeable – CoroutineScope(CoroutineScope(CoroutineScope.coroutineContext).coroutineContext).coroutineContext
CoroutineScope()
function will always return a scope with a context that has a Job
– if the context passed to it does not have a Job
, an orphan one will be created.Carrascado
08/26/2020, 4:44 PMCoroutineScope
, isn't it a constructor?Zach Klippenstein (he/him) [MOD]
08/26/2020, 4:45 PMCarrascado
08/26/2020, 4:49 PMwithContext(<http://Dispatchers.IO|Dispatchers.IO>)
? That makes you guess that built in dispatchers (IO, Default, etc.) are actually some context instances... but in that case, how can that work? Those instances will probably have their own Job instance, their own everything.
If the code inside withContext
runs within a different context, then it will run within a different coroutine because the Job is different isn't it? 🤔Zach Klippenstein (he/him) [MOD]
08/26/2020, 4:59 PMJob
is a context element. A dispatcher is a context element too. A context element is a context that only contains one element.Carrascado
08/26/2020, 5:03 PMZach Klippenstein (he/him) [MOD]
08/26/2020, 5:14 PMCarrascado
08/26/2020, 5:15 PMZach Klippenstein (he/him) [MOD]
08/26/2020, 5:15 PMoldContext
is a map, context
is a map, oldContext + context
is like +
-ing any maps – the keys in the left one are “overwritten” with the same keys in the right one.Carrascado
08/26/2020, 5:16 PM