https://kotlinlang.org logo
#coroutines
Title
# coroutines
l

Lukasz Kalnik

03/15/2024, 6:46 PM
Why does a
while
loop have to check cooperatively for cancellation (e.g. using
while (isActive)
), whereas "linear" (i.e. non-loop) code doesn't? 🧵
Here, the coroutine will be just canceled before the
println()
statement:
Copy code
suspend fun main() {
    val job = CoroutineScope(Job()).launch {
        delay(50)
        println("after delay") // never gets here, because coroutine got canceled
    }
    delay(30)
    job.cancel()
}
s

Sam

03/15/2024, 6:49 PM
It's the
delay
that does the check for you.
thank you color 1
☝️ 2
If you put a
delay
in your loop, it will check there for you too and you won't need
isActive
.
k

kevin.cianfarini

03/15/2024, 6:50 PM
infinite loops don’t have to be cooperative unless they’re doing something that doesn’t check for cancellation. Eg.
Copy code
while (true) {
  delay(1_000)
  someSynchronousOperation()
}
versus
Copy code
while (isActive) {
  someSynchronousOperation()
}
will both respect cancellation. The former does the check in the
delay
while the latter explicitly checks for cancellation cooperatively
s

Sam

03/15/2024, 6:50 PM
All the standard suspending functions include the cancellation check.
k

kevin.cianfarini

03/15/2024, 6:50 PM
Manually doing cooperative cancellation is usually only done at the barrier of coroutines and something else.
l

Lukasz Kalnik

03/15/2024, 6:51 PM
Hmm, so if I put only non-checking for cancellation functions inside a coroutine, it will run through, unless I intersperse them with
ensureActive()
or
yield()
?
s

Sam

03/15/2024, 6:51 PM
Yes, exactly.
l

Lukasz Kalnik

03/15/2024, 6:53 PM
Thank you for the quick and helpful answer.
I have been working with coroutines for so long and never understood the difference 🙈
I think it's quite non-obvious that
delay()
and similar helper functions are "special" in this regard and "good citizens".
k

kevin.cianfarini

03/15/2024, 6:54 PM
I very, very rarely write code that does manual cooperative cancellation. You shouldn’t have to do it often.
l

Lukasz Kalnik

03/15/2024, 6:55 PM
I just had to do an endless loop polling a server every 3 seconds...
Because of outdated backend infrastructure
k

kevin.cianfarini

03/15/2024, 6:56 PM
Well, anything that bottoms out in
suspendCancellableCoroutine
will do the cooperative checks for you. It just so happens that some coroutines don’t necessary check for cancellation when they don’t bottom out in
suspendCancellableCoroutine
. Things like:
Copy code
launch(<http://Dispatchers.IO|Dispatchers.IO>) { someBlockingSynchronousOperation() }
s

Sam

03/15/2024, 6:56 PM
Since, in effect, almost all suspension points are ultimately based on one of the built in cancellable suspending functions, you'd have to work hard to find/create one that doesn't check for cancellation. But I agree, it's not totally intuitive.
☝️ 1
l

Lukasz Kalnik

03/15/2024, 6:56 PM
Indeed when I replace
delay()
with
Thread.sleep()
the coroutine just runs through:
Copy code
suspend fun main() {
    val job = CoroutineScope(Job()).launch {
        Thread.sleep(500)
        println("after sleep") // never gets here, because coroutine got canceled
    }
    println("after launch")
    delay(10)
    job.cancel()
    println("after cancel")
    delay(1000)
}
What do you mean with "everything that bottoms out in `suspendCancellableCoroutine`"?
k

kevin.cianfarini

03/15/2024, 6:57 PM
Let me back up for a second to explain that. It will require some typing:
A
suspend
function is just a fancy callback. Bridging between callbacks and suspending function is done with the
suspendCancellableCoroutine
function. Examples look like the following (an example from OkHttp)
Copy code
suspend fun Call.executeAsync(): Response =
  suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      this.cancel()
    }
    this.enqueue(
      object : Callback {
        override fun onFailure(
          call: Call,
          e: IOException,
        ) {
          continuation.resumeWithException(e)
        }

        override fun onResponse(
          call: Call,
          response: Response,
        ) {
          continuation.resume(value = response, onCancellation = { call.cancel() })
        }
      },
    )
  }
OkHttp provides a native API for async operations with callbacks, but this function bridges it to native suspending function with the
suspendCancellableCoroutine
function. So does
delay
,
yield
, and plenty of other suspending functions. Any function which calls
suspendCancellableCoroutine
will inherently cooperatively check for cancellation
👍 1
l

Lukasz Kalnik

03/15/2024, 7:00 PM
I actually had this code for polling:
Copy code
val pollingStopTimeMark = TimeSource.Monotonic.markNow() + 30.minutes

val job = coroutineScope.launch {
    while (isActive) {
        if (pollingStopTimeMark.hasPassedNow()) {
            job.cancel()
        } else {
            pollBackend()
            delay(3.seconds)
        }   
    }
}
The problem was it fell in an infinite loop after the
pollingStopTimeMark
has passed, trying to cancel itself infinitely, although it was already canceled.
Although I'm not sure if we used
while (isActive)
there, maybe it was an
ensureActive()
check inside the
while
loop.
k

kevin.cianfarini

03/15/2024, 7:03 PM
I’m not sure where the bug lies in that code sample off the top of my head, sorry
👍 1
Though it doesn’t feel like the bug here is cooperative cancellation.
It sounds like
pollingStopTimeMark.hasPassedNow()
should be a loop condition, and not cancel the coroutine
If you’re curious about the concept of cooperative cancellation these docs are a good place to read. They’re actually pretty good. https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
s

Sam

03/15/2024, 7:05 PM
I agree about the loop condition--a coroutine that cancels itself can certainly cause some unexpected weirdness, and is something I'd try to avoid
l

Lukasz Kalnik

03/15/2024, 10:10 PM
Thank you both for the suggestions! We actually in the end just used the
withTimeout
coroutine function instead of checking the
pollingStopTimeMark
, and it worked correctly.
👍 1
2 Views