I'm trying to learn coroutines and I found a stran...
# announcements
s
I'm trying to learn coroutines and I found a strange thing. If I save a reference to CoroutineScope to a field, and then a method uses this field to launch some jobs, it seems to be unable to cancel such jobs. My example has about 40 lines, I don't want to break some mobile user's screen, so it is linked here: https://gist.github.com/StragaSevera/b294625b927eb002be8987804de31013 This code should stop counting when I press "Enter", but it does not cancel the coroutines and counts forward after the "Cancelled" message 😃 Also, the bug happens if I pass the scope as an argument to the
start()
method, but does not happen if I remove the class and copy contents of the
start()
method to the
main()
function. It seems like there is some caching issue...
i
You might have better luck in #C1CFAFJSK
z
I don’t see a cross-post there, so I’ll respond here. The problem is that you’re not taking advantage of structured concurrency. Your
countTo
coroutines are running as children of your
scope
property, not as children of the coroutine running
calculate
. So when you cancel the coroutine that the
calculate
function is running in, it doesn’t do anything. To fix this, and in general, whenever you have a
suspend
function, any coroutines you launch should usually be children of the current scope, not of an external one. You can get the current scope in a suspend function using the
coroutineScope
function. Your
calculate
function should look like this:
Copy code
private suspend fun calculate(): Int {
      // Wrap with coroutineScope.
      return coroutineScope {
        // Now, inside this lambda, this is the CoroutineScope of the current
        // coroutine, so you can call async without an explicit receiver and it
        // will launch a child of the current scope.
        val a = async { countTo(10) }
        val b = async { countTo(20) }
        // Need a qualified return now since this is in a lambda, but
        // you could also just omit the return keyword entirely.
        return@coroutineScope a.await() + b.await()
      }
    }
This also takes advantage of one of the other features of structured concurrency – the
coroutineScope
function won’t return until all the coroutines launched inside it have completed, which means it’s much harder to accidentally forget to join a coroutine and leak it.
Or more concisely:
Copy code
suspend fun calculate(): Int = coroutineScope {
    val a = async { countTo(10) }
    val b = async { countTo(20) }
    a.await() + b.await()
  }
s
I did not know about that channel, thanks 😃 @Zach Klippenstein (he/him) [MOD] Hmm, so any call of a suspend function starts a new coroutine? I didn't know that, thank you. So each and every function that uses
async
should be wrapped in
coroutineScope
?
y
so any call of a suspend function starts a new coroutine
No, it's specifically that any call that starts a coroutine (i.e. any call that's defined as an extension on coroutineScope) should be wrapped in a coroutineScope block if inside a normal suspend function. If you call any other suspend function then you don't need that coroutineScope block, it's only needed here because
async
is a call that starts a coroutine
s
Ah, got it, thanks. So I should never pass Scope explicitly? Or is there any circumstance where passing Scope is preferrable?
z
Generally, a function should either be suspending, OR take an explicit
CoroutineScope
– not both.
s
Hmm, in which case I should prefer the latter?
z
If you need the function to kick off some work then return immediately, then pass the scope in. If you want your function to do some work and only return when the work is done, then it should be a suspend function.
s
hmm, got it, thanks 😃