Why does a `while` loop have to check cooperativel...
# coroutines
l
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
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
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
All the standard suspending functions include the cancellation check.
k
Manually doing cooperative cancellation is usually only done at the barrier of coroutines and something else.
l
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
Yes, exactly.
l
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
I very, very rarely write code that does manual cooperative cancellation. You shouldn’t have to do it often.
l
I just had to do an endless loop polling a server every 3 seconds...
Because of outdated backend infrastructure
k
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
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
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
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
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
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
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
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