Is there an easy way to start a coroutine from a n...
# coroutines
t
Is there an easy way to start a coroutine from a non-suspend function? E.g.
Copy code
fun justSomeFunction() {
  launchACoroutine(<http://Dispatchers.IO|Dispatchers.IO>) {
    while (true) {
      // just do some work
    }
  }
}
n
Do you want
justSomeFunction
to block until the work is done? Is
justSomeFunction
called as part of some larger process that will want to know when the work finishes, or that may be cancelled and so this work should be cancelled too?
t
Oh! Good question. I want
justSomeFunction
to return as soon as the coroutine is launched. Fire and forget.
n
You are asking for GlobalScope. Be sure to read the fine print and for the related CoroutineScope in case that's a better fit for you (it usually is). "Fire and forget" is an easy way to create problems for yourself.
b
Coroutine(Dispatcher.IO).launch { //Code }
g
I wouldn't recommend to use this Coroutine(Dispatcher.IO), if you really need a top level Coroutine it's better to use GlobalScope.launch(IO) at least you explicitly use global scope and create top level Coroutine, also no need to create useless scope for one coroutine
f
for testing purposes you should consider creating your own global coroutine scope (maybe something you can inject) so that you can change the dispatcher during tests
j
If you fire and forget, you need to think about cancellation. Usually there are 2 main options: the first one is to provide a scope from outside this function, and cancel the scope when you don't need the coroutines that were launched in it anymore. The other major option is to use the
GlobalScope
, but then you're outside structured concurrency so you need to return the
Job
from your function (the one that
launch
returned to you) so you can cancel the coroutine manually when necessary
g
So in short answers here should be separated on 2 ways: 1. use GlobalScope or own global level scope to help with testing as Francesc suggested 2. You probably shouldn’t do this and avoid such code in all cases, it has issues with cancellation and testing
d
I have had better luck using one (or more) custom global scopes - easy 1-liners to create. This way I can tell the difference between problems caused by code that 'accidently' used GlobalScope vs code that was 'managed' with my own. I usually use SupervisorJob becuase the semantics makes more sense to me -- Biggest problem with Globalscope, IMHO, is if you ever cancel it by mistake your screwed. With your own scope then you limit the potential damage of errant code canceling the scope
j
How do you cancel
GlobalScope
by mistake?
g
You cannot cancel GlobalScope, because it doesn't include Job which you can cancel But if you have own scope with SupervisorJob there is a chance that it can be accidentally cancelled
@Joffrey it's not so hard how it looks, if you pass scope to other classes in complex code you may get situation when one of classes cancels scope (it's a bug, you usually shouldn't cancel provided scope of course and instead create child ones)
j
Exactly, it's just wrong to cancel a scope that you don't own / haven't created
g
Well yes, but sometimes you want to cancel all tasks which you run and may accidentally cancel it I would actually like to have special interface which prevents cancelling it
j
An interface without
cancel()
will not really help you much here. If you have classes that accept an external
CoroutineScope
, and that make the mistake of cancelling it, why would they not make the mistake of using the
CoroutineScope
interface instead of the new non cancellable interface? The same problematic code could persist on the consumer side. The problem is not accidental cancellation IMO, the intention of cancelling an external scope is a mistake in itself, not an unintended side effect of something else
Sometimes you want to cancel all tasks
In that case you use structured concurrency, and cancel the scope that you created yourself for this very purpose, not an externally provided scope.
GlobalScope
is giving up on structured concurrency, and has an opt-in requirement to make sure you understand this before using it. So I don't see
GlobalScope.cancel()
ever happening directly either (but that wouldn't be accidental anyway)
g
GlobalScope.cancel() will not do anything, which is good and bad the same time
Yeah, I know how to handle structured concurrency, but when you have 10+ people who work on a project it becomes harder to catch it, but code review helps
Why wouldn't it help? If default scope has no cancellation method by default it makes it harder to make this mistake
n
Jobs don't provide access to their parent. A scope that can't cancel others is just a "child" scope. You could leverage the type system with something like:
Copy code
class ChildCoroutineScope(private val parent: CoroutineScope): CoroutineScope by parent + Job(parent.coroutineContext[Job])
(untested just typed it out) FYI, I've run into issues mixing up scopes when I have more than one, a
scope
parameter and a
this
launched scope and can easily see someone cancelling the wrong one.
j
We are discussing several things at the same time. I had 2 points about @DALDEI’s "accidental cancellation of
GlobalScope
". 1. I don't believe anyone ever tries to call
GlobalScope.cancel()
directly at all. If that happens, it's not an accident (as in an unintended consequence of a correct intention), it's a mistake (the intention itself was wrong). 2. The only way that cancelling the
GlobalScope
could happen potentially by accident would be if some code receives a scope from elsewhere as
CoroutineScope
, and tries to cancel it. That's also IMO just an incorrect intention in the first place (with any scope, not just when
GlobalScope
is passed). So, yes, that could lead to accidental cancellation of the
GlobalScope
, but it's first and foremost a bigger mistake about cancelling externally-provided scopes.
Why wouldn't it help?
@gildor I thought what you were suggesting is just a new interface (a super interface of
CoroutineScope
) that wouldn't have a
cancel()
method. If the problematic code asks for a
CoroutineScope
and chooses to cancel it, the fact that another interface is available doesn't prevent this from happening (they could still decide to ask for a
CoroutineScope
instead of the new interface). But what I agree that if
GlobalScope
didn't implement
CoroutineScope
anymore, then it could not be passed to those incorrect pieces of code. I am not sure
GlobalScope
could be changed this way. I also believe it would not be worth the breaking change given that it prevents an arguably small accident from code that has bigger problems anyway.
FYI, I've run into issues mixing up scopes when I have more than one, a
scope
parameter and a
this
launched scope and can easily see someone cancelling the wrong one.
@Nick Allen That's fair, although I have trouble imagining a situation where one needs to cancel "this" scopes from locally launched coroutines. So I'd argue neither should be cancelled? Maybe it's just wishful thinking, and valid situations like this do arise.
g
If the problematic code asks for a
CoroutineScope
and chooses to cancel it,
It’s correct, but it will be one more level of protection. Also on DI environment it’s very easy, you can do not add cancellable version of top level context
worth the breaking change given that it prevents an arguably small accident from code
You can say exactly the same about List vs MutableList I believe there are valid ways to migrate in 2-3 versions, no need to break existing code
You could leverage the type system with something like:
Yes, this what we are doing, we even have helper builder for this (probably worth to request adding it to kotlinx.coroutines) Want to clarify, I understand how to work with it, I just also have experience in very big project fully based on coroutines on which multiple developers work, and no,
The only way that cancelling the
GlobalScope
could happen potentially by accident
But GlobalScope will not be cancelled! It doesn’t have job, nothing to cancel. You cannot cancel it in any way, all jobs started from it have Job without parent. My original comment was about David’s message, that they use custom scope with SupervisorJob, which is indeed one of ways to handle it (and we use this approach too, for example we have UserScope, which started on login and cancelled on logout), my concern is that this approach, although we use it, is error prone in a very nasty way, when scope can be accidentally cancelled by any code, so it’s better to at least always provide new child scope, so it will prevent code which injected it, to cancel all the jobs
d
I was mis-thinking saying 'Canceling GlobalScoper" -- I was really meaning the combination of the jobs created within GlobalScope are not canceled by calling cancel() on the parent (GlobalScope) On discovering later that these coroutines keep running after they were expected/assumed to be and then do bad things like accessing now dead objects .. A developer may read up to discover the 'bad' GlobalScope and then find some code that does CoroutineScope(Dispatchers.IO) as a 'application global scope' --| then not fully comprehending 'the fine print' have code that calls cancel() on that scope -- OR an exception in a child scope ends up not only canceling all the siblings and the parent - but the side effect of the propagated cancel of the custom scope -- is that it silently fails run more coroutines. So we have GlobalScope -- produces unmanageable orphan coroutines that keep running and "The Improved Convention" make your own CoroutineScope() === later (in production of course!) that after the first error in the app nothing else happens -- all calls to launch() or async() simply fail quietly to run Of course one would correctly say "Thats how its documented to work" To which Ill answer 'yes' -- but --that is not the same as 'obvious' or even 'reasonably assumed' It took me over a year to finally get this -- and I can assure one that noone else on my team does The nuances of structured scopes is extremely subtle and confusing -- to some people -- And while in hindsight -- it makes good sense -- once you are fully indoctrinated -- but until then -- and even after -- its still not 'obvious' -- good sense or no -- that the default (and assumed 'recommended') scope properties are such that exceptions kill siblings and parents -- it is 'obvious' that cancelation goes down -- that a cancel of a parent cancels all its children -- but an exception in a child canceling the siblings and parent -- is bizzare SupervisorJob() behaves how I natural assumed' that CoroutineScope() or GlobalScope would That is the exact opposite of the truth -- similar to many things in kotlin and coroutines -- they seem to be things that make good sense only after you are very experienced in the language This makes learning them difficult Much like Gradle - which IMHO only begins to make any sense at all AFTER you know it in depth If you do not veer from the golden path -- just copy examples from elsewhere -- all works magically beautiful -- but change one thing -- or try to create from scratch -- and that beauty and simplicity as magically vanishes -- until you deeply learn everything
1