DALDEI
10/03/2021, 3:56 AMSatyam Agarwal
10/03/2021, 5:41 AMephemient
10/03/2021, 5:45 AMrunBlocking
or equivalentrunBlocking { ... withContext(Dispatchers.Main) { ... } }
will deadlockDispatchers.Main.immediate
will run until the first suspension point, but that is hardly any guaranteeSatyam Agarwal
10/03/2021, 5:53 AMephemient
10/03/2021, 5:56 AMonCreate
, and that may require restructuring your app so that you can start the work but finish it later. but the alternative is painful debugging when you do end up deadlocking.
• on the server side, most existing frameworks are thread-per-request. in that case runBlocking
to adapt between the suspending and non-suspending worlds is safer. your suspend funs should withContext
to move themselves into an appropriate dispatcher (e.g. <http://Dispatchers.IO|Dispatchers.IO>
or Dispatchers.Default
) instead of pushing that knowledge into the caller.runBlocking
, as much as possible. this will impose some order on how you convert an existing codebase to coroutines in a way that is tractable, IMO.CLOVIS
10/03/2021, 6:38 AMcoroutineScope
Adam Powell
10/03/2021, 3:05 PMrunBlocking {}
is something that should appear at very coarse-grained boundaries and very infrequently. Usually these are entry points, like fun main() = runBlocking { ... }
or
@Test
fun myCodeBehavesAsExpected() = runBlocking {
// test code
}
and yes, sometimes you might have exactly one runBlocking
right at the beginning of handling a request for a thread-per-request server framework.lifecycleScope.launch { ... }
which will automatically cancel when the activity is destroyed. You can use things like
lifecycleScope.launch {
repeatOnLifecycle(STARTED) {
someDataFlow.collect {
someView.text = "New value is $it"
}
}
}
and repeatOnLifecycle
will subscribe and unsubscribe to the flow each time the activity is started/stopped, respectively.suspendCancellableCoroutine
to write a suspend
adapter for it so that you can keep callers thinking in suspend
- they'll already bring appropriate scopes with them.withContext(<http://Dispatchers.IO|Dispatchers.IO>)
to dedicate a temporary thread to the blocking call.runBlocking {}
. And even then, something like Android's onCreate
is not an example of this; onCreate
shouldn't block. If you need to do something sufficiently over-time enough that it was written as a suspend call, use something like lifecycleScope
.Activity.onCreate
is again, not an example of this.DALDEI
10/03/2021, 11:05 PMIf you have non-coroutines code that blocks the calling thread on external IO, you can wrap them in suspend using withContext(Dispatchers.IO) to dedicate a temporary thread to the blocking call.Um, no, that does not work. withContext() can NOT be called from "non-coroutine code" Please prove Im wrong, that would make my day x 10000 fun nonCoroutineCode() { withContext( Dispatcher.IO ) { ---- DOES NOT COMPILE } } And that is the crux of the problem -- all these great methods only work if your within a suspend fun
Another example:
That the beauty of this framework, that the child coroutine is aware of parents coroutine context and runs in the same unless stated otherwise.
So unless you have changed the coroutine context somewhere down the call chain, all the coroutines should run in the same context.Um, no. fun coroutineCode() { scope.launch { fun1() } } ---- some other module fun fun1() { fun2() } fun fun2() { launch { } // DOES NOT COMPILE (or any other builder ) - } If you leave the lexical scope of an in scope CoroutineScope or you are in a a non-suspend fun - your out of luck -- no coroutines for you unless you aquire a scope somehere (which is not hard -- but -- I''d LIKE it to be the CALLERS scope -- but it cant be withoujt passing it down becuase its not a suspend fun so no 'scope is in scope' ---------- This answer is the only one that I belive is correct:
if you must block non-suspending code from returning, your only choice isor equivalentrunBlocking
ephemient
10/04/2021, 1:07 AMAdam Powell
10/04/2021, 2:38 PMoverride fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
performSuspendingInit()
doThingsThatRequireSuspendingInitToBeComplete()
}
}
If you are in a suspend function and you need to launch something, use coroutineScope {}
instead of going hunting for another external scope to launch into:
suspend fun myFunction() {
coroutineScope {
launch { doThingOne() }
launch { doThingTwo() }
}
}
structured concurrency means async behavior is always opt-in; launching into your caller's scope is an antipattern because it "leaks" running operations into your caller. By writing it as the above, the caller can wrap it in their own launch {}
call if they want it to happen concurrently with something else they're doing, and they can use exactly the same coroutineScope {}
pattern to get a scope.suspend
function and you need to do something over time, then you need to think through whether you want to launch the operation into a scope or if you want to send a message to a suspend
function that's already running via a Channel
or similar mechanism.DALDEI
10/05/2021, 3:32 AMmain() {
normalFunction()
}
fun normalFunction() {
someScope.launch {
callSuspend()
}
callNonSuspend()
}
suspend fun callSuspend() {
callNonSuspend()
}
----------------------------->>> >HERE
fun callNonSuspend() {
// Imagine Today I want to ADD a call some to suspend fun --
whatToDoToCallSuspend()
}
------------
Certainly there are many was to do this -- but the problem isnt so much how, but what. It makes a difference if 'callNonSuspend' was called 'from a coroutine' or not.
Things like exception handling - should they be propagated to the caller scope or not, if a coroutine is launched but not waited for, who is going to manage it , whether its 'OK to Block Thread' -- which can lead to deadlock not just inefficiency.
These are not fundamentally different then other architectural questions - but they add a whole new dimension to 'WTF' -- If your adding coroutines to an existing program then its unlikely the previous authors thought about coroutines at all, or in the same way. The variety and effect of interaction between otherwise unrelated code is substantial when you add coroutines.
However one other factor makes this more specific to both kotlin and coroutines.
The heavy use of Scope* conventions.
By this I mean the kind of "DSL Scope" that coroutine builders use, similar to the more idiomatic scope functions (let, with, use, apply etc). These leverage the compiler in ways that are profoundly powerful -- but *only work when the entire call chain is in scope
e.g This works
MyScope.launch {
async {
coroutineScope {
launch {
async { ... }
}
}
}
All very nicely and cleanly hide the implicit scope so they 'just work"
But when the same exact code gets distributed across files and modules --
it falls apart dramatically. There is no implicit scope, or its the wrong one.
What was enforce/supplied by the compiler is gone. You have to explicitly recreate the implicit scope and carry it through everything.
No different then other kotlin scope usage -- yet fundamentally different in that the current version of coroutine APIs require it -- you end up having to explicitly implement the receivers to every level of function call just to use it 1 time 10 levels deeper. All the hard work of the library authors vanishes the moment you leave the lexical scope. Building the scaffolding back up is not trivial.
An approach I have not yet tried but may -- is using the newly supported ThreadLocal coroutine context. This could help a LOT -- but I havent fully walked through all the details.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-thread-context-element/index.html
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html
Basic concept
fun CoroutineScope.topLevel() {
put "this" into a thread local
fun2()
}
....
fun fun100() {
Look for scope or context from thread local
with( myThreadLocalScope ) {
Yea Life is restored
}
}
ephemient
10/05/2021, 7:41 AMDALDEI
10/18/2021, 11:17 PMephemient
10/19/2021, 12:58 AMDALDEI
10/29/2021, 1:23 AM