hey folks, could anyone explain if there is would ...
# coroutines
g
hey folks, could anyone explain if there is would be any difference between these 2 ways of declaring a
CoroutineScope
?
Copy code
val childScope1 = CoroutineScope(this.coroutineContext)
val chileScope2 = coroutineScope {}
d
The second is not even a thing. Maybe you meant
coroutineScope { this }
?
g
in this case I'm inside a
suspend
block
s
coroutineScope {}
is a suspending function, and it will wait for all its child coroutines to complete before returning. •
CoroutineScope(this.coroutineContext)
won't wait for anything, so its new coroutines will run in the background. This can be confusing if you're inside a suspending function, because callers expect the suspending function to finish all its work before returning. So you should prefer to use
coroutineScope {}
.
g
Thx! I'm looking to create a child scope with a smaller lifecycle. So my current thought is injecting the parent scope and creating
val childScope = CoroutineScope(parentScope.coroutineContext)
but then I have a public
suspend fun doSomething()
which might be called in a coroutine running in the parent scope, but I could do
childScope.launch()
and return immediately. So that way parent can continue doing work. Does that seem to make sense?
I still haven't wrapped my head around all the concepts around between
Scope
and
Context
s
Trying to create a
CoroutineScope
using an existing
coroutineContext
from another suspending function or scope is almost never going to be a good idea, and will probably have unintuitive or unexpected results. The
CoroutineScope
constructor function is intended for occasional use by classes that manage their own lifecycle and their own coroutine context elements. And as I said, by convention, a suspending function should complete all of its work before returning. If you want a function that does some background work, it should either use its own scope (perhaps owned by its class) or accept an existing coroutine scope as an input (e.g. a receiver).
I realise that sounds like a lot of "no, don't do that" 😄
👍 1
g
"no, don't do that" sounds great when you are not sure of what you are doing, so I really appreciate that!
s
Can you explain a bit more about what you're trying to achieve? Then maybe we can get to a "do this instead"
💯 2
g
I'm trying to implement a State Machine, which receives events from outside, queues them and handles them to changes state and stuff.
I really like the fact that I can get backpressure from collecting a Flow or a Channel, so I'm leaning on that direction a bit
so the StateMachine is initiated from a ViewModelScope from Android for example, but I want a single State to have a short lived lifecycle to run things
so it can for example, handle an event by triggering a new coroutine to do work in the background, but return that the event was already handled
but when the state exits, it could cancel it's own scope
s
I see—so it has a lifecycle of its own, but should also be parented by the view model scope and be cancelled if that scope terminates?
g
exactly
I have to go for a bit, but i'd really appreciate some pointers if there are any examples or docs on how to handle "classes that manage their own lifecycle and their own coroutine context elements"
thx a lot for the help so far, I'll be back later 🙂
s
What you're looking for is pretty much an actor—a coroutine that receives messages from outside. There is an
actor
function in the coroutines library, but it's sort of deprecated without replacement, which is a bummer 😞. That said, its moving parts aren't very complicated. You could achieve something pretty similar (if not better) just by making your own channel and coroutine. I'd do it like this:
Copy code
class MyStateMachine {
  private val queue = Channel<Stuff>()

  fun postEvent(event: Stuff) {
    queue.trySend(event)
  }

  fun endOfInput() {
    queue.close()
  }
  
  suspend fun execute() {
    queue.consumeEach { message ->
      doStuff(message)
    }
  }
}

val myStateMachine = MyStateMachine().also {
    viewModelScope.launch { it.execute() }
}
Calling
endOfInput()
closes the channel, which ends the loop and allows the coroutine to terminate—so there's no need for any extra lifecycle management beyond that.
s
If you're interested, I wrote this blog post about CoroutineScopes. Towards the end, there is a paragraph about when you want to deal with objects/components that have different lifecycles, ie CoroutineScopes: https://medium.com/swlh/how-can-we-use-coroutinescopes-in-kotlin-2210695f0e89 I never got around writing a follow up explaining when to call
coroutineScope { ... }
inside a suspend fun. That one is basically to allow you to do asynchronous/parallel stuff inside the suspend fun and within the same CoroutineScope as the one that called the suspend fun.
👍 1
g
thx a lot Anton! that definitely makes it more clear! I was a bit confused about that indeed
thx a lot Sam, that's close to what I'm already doing to handle the queue, but I'd still like to have that scoped state like we described, but I feels it's a somewhat complicated thing to achieve.
s
Using a channel, you should be able to signal all the necessary cancellation/error states without any explicit link between the scopes 👍
👍 1
g
Just finished reading your article Anton, this was great! really cleared up a lot of the confusion in my head.
👍 1
So the conclusions I'm getting so far: 1. Looks like there isn't really a way to have nested
CoroutineScope
like I wanted to =/ 2. It should be safe to create my own
CoroutineScope
with something like `CoroutineScope(Dispatchers.Default + Job())`as long as it's independent and only relies on the lifecycle of the class it belongs to. 3. Interactions between scopes have some interesting caveats
Hey Sam, about this part : > Trying to create a
CoroutineScope
using an existing
coroutineContext
from another suspending function or scope is almost never going to be a good idea Can you elaborate a bit? I understand this breaks the expectations from the caller in a simple suspend function, but if I let's say inject the parent scope and use that to make my new one, does that seem like a bad idea? I'm thinking of something like if I have
Copy code
val parentJob = Job()
val parentScope = CoroutineScope(Dispatchers.Default + parentJob)
If I do
val childScope = _CoroutineScope_(parentScope.coroutineContext)
then they will essentially be the same scope? because the whole context is the same. But if I do something like this, it feels like it would be closer to what I wanted
Copy code
val childJob = SupervisorJob.(parentScope.coroutineContext.job)
val childScope = CoroutineScope(parentScope.coroutineContext + childJob)
Hey, good morning 🙂
following up, at the very least I think this should be OK no?
Copy code
val childJob = SupervisorJob(parentScope.coroutineContext.job)
val childScope = CoroutineScope(Dispatchers.Default + childJob)
s
When I said that "trying to create a
CoroutineScope
using an existing
coroutineContext
from another suspending function or scope is almost never going to be a good idea", I was mostly thinking of this kind of thing:
Copy code
suspend fun foo() {
  val scope = CoroutineScope(coroutineContext) // don't do this
  scope.launch { bar() } // this leaks from the function
}
It sounds like what you're describing is more like creating a custom scope directly as a child of another custom scope—that is, without any suspending functions involved. That might be less problematic in terms of encapsulation, but I think it could still be quite confusing for people reading the code. Structured concurrency is useful because it's predictable and consistent, and if you start trying to make up your own rules for how it should work, you lose some of that.
👍 1
Your most recent example will work, in the sense that it'll create the relationship you want between the jobs, but I still wouldn't do it. Scopes are for launching coroutines, and attaching random extra jobs to a scope in other ways seems like a recipe for confusion.
What specific problem are you running into that you can't solve without doing this? From what you've said so far, it sounds like you're worried things could go wrong if your scopes aren't linked up somehow?
g
Awesome! Even thought it feels I'm still doing something wrong at least I'm learning quite a lot! 🙂 Yeah, so my Actor has a mix of sequential work to execute, and async interruptions that might interrupt or modify the sequential work. So the
Channel
would be receiving external events and spawning a new Coroutine to do the work. Let's considers something like this:
Copy code
public suspend fun start() {
    eventChanel.receiveAsFlow().collect {event ->
This doesn't work because I need to spawn the collection in a new Coroutine, otherwise start() will suspend forever. And I can't just call
launch
from a suspend function and assume the caller's scope. Which is good because the convention is that spawning new coroutines needs to be done from a scope. So instead maybe I should do.
Copy code
val myScope = CoroutineScope(Dispatchers.Default + Job())   

public fun start() {
    myScope.launch {
        eventChanel.receiveAsFlow().collect {event ->
But at this point I fully rely on calling
stop()
to cancel
myScope()
, which is fine, I think this is the intended design. But this class lives in a
ViewModel
, so if that gets cleared, I need to make sure to trigger
stop()
manually in a class that is like 4 levels of abstraction deeper.
Or maybe I would send an event to the Channel to cancel myScope, but that might already also have stuff in the queue to be processed and leak.
Then I start falling into stuff like this
Copy code
public fun start() {
    myScope.launch {
        eventChanel.receiveAsFlow().collect {event ->
        when(event) {
            SomeEvent -> {
                // do something
                myScope.launch{
                    // start work in background to make sure I can collect new events
                }
            }
            StopEvent -> {
                myScope.cancel()
            }
        }
    }
}
s
Making
start()
a suspend function would definitely be my preferred solution. It means you don't need a
stop()
function—you just cancel the coroutine that's running the
start()
function instead. You're right that this would mean you need to start it in its own coroutine. So, are you able to do that directly in the
viewModelScope
, after creating the instance? Something like this:
Copy code
val myActor = MyActor()
val job = viewModelScope.launch { myActor.start() }
g
Not really, ViewModel and MyActor don't really know about each other, I'd like them to just send and receive messages when needed.
what we did right now, which I'm not sure is a good idea, is that we are passing.
viewModelScope
to other classes to initialise them.
Copy code
override fun initialize(scope: CoroutineScope) {
        scope.listenToDeviceCallbackEvents()
private fun CoroutineScope.listenToDeviceCallbackEvents() {
s
I don't see a problem with that, it's a common enough pattern. We see it in the standard coroutines library, for example in
Flow.launchIn(scope)
or
Channel.produceIn(scope)
. More broadly, we can divide asynchronous functions into two groups: 1. Suspending functions, which do all their work before returning. These have the
suspend
modifier and should never leak coroutines. 2. Background tasks, which take a coroutine scope as a parameter (or receiver) and return right away, while starting a new concurrent task. These never have the
suspend
modifier.
👍 1
What makes you concerned that passing the viewModelScope could be a bad idea?
(By convention, functions that take a coroutine scope as a parameter might also have "async" in the name and might return a
Job
, but I would say those are both optional extras)
g
It's a bit cumbersome in terms of dependency injection, and then I need to keep track of the launched jobs individually I think.
Copy code
public fun startActorAsync(scope: CoroutineScope): Job {
    val runningJobs = mutableListOf<Job>()
    return scope.launch {
        eventChanel.receiveAsFlow().collect { event ->
            when (event) {
                SomeEvent -> {
                    // do something
                    val job = scope.launch {
                        // start work in background to make sure I can collect new events
                    }
                    runningJobs.add(job)
                }

                StopEvent -> {
                    runningJobs.forEach{it.cancel()}
                }
            }
        }
    }
}
But this also doesn't look the best
Copy code
private lateinit var myScope: CoroutineScope
public fun startActorAsync(scope: CoroutineScope) {
    myScope = CoroutineScope(Dispatchers.Default + Job(scope.coroutineContext.job))
    myScope.launch {
        eventChanel.receiveAsFlow().collect { event ->
            when (event) {
                SomeEvent -> {
                    // do something
                    myScope.launch {
                        // start work in background to make sure I can collect new events
                    }
                }
                StopEvent -> {
                    myScope.cancel()
                }
            }
        }
    }
}
public fun stopActor() {
    if (this::myScope.isInitialized) {
       myScope.cancel() 
    }
}
I think anyway I'd always need some logic to make sure things are not started twice for example.
s
Well, I can't help with the dependency injection part, but you can rely on structured concurrency to keep track of the launched jobs for you. When you handle the individual events, don't use
scope.launch()
again—just use
launch()
. This will make the new jobs be children of the outer job. Then, when you get the
StopEvent
, you can just
cancel()
the whole job, and it will automatically cancel its children.
Copy code
fun startActorAsync(scope: CoroutineScope): Job = scope.launch {
  eventChannel.receiveAsFlow().collect { event ->
    when (event) {
      SomeEvent -> launch { /* start work in background */ }
      StopEvent -> cancel()
    }
  }
}
That's assuming that you need a dedicated
StopEvent
. If you can signal the end just by closing the channel instead, things could be even simpler.
👍 1
If you're worried about making sure it only starts once, you could consider making the constructor private, and exposing it only through a public function that launches the job at the same time as creating the instance.
g
thx a lot! I really think I have a much better idea of what this could look like 🙂