Lukasz Kalnik
03/15/2024, 6:46 PMwhile loop have to check cooperatively for cancellation (e.g. using while (isActive)), whereas "linear" (i.e. non-loop) code doesn't? 🧵Lukasz Kalnik
03/15/2024, 6:48 PMprintln() statement:
suspend fun main() {
val job = CoroutineScope(Job()).launch {
delay(50)
println("after delay") // never gets here, because coroutine got canceled
}
delay(30)
job.cancel()
}Sam
03/15/2024, 6:49 PMdelay that does the check for you.Sam
03/15/2024, 6:50 PMdelay in your loop, it will check there for you too and you won't need isActive.kevin.cianfarini
03/15/2024, 6:50 PMwhile (true) {
delay(1_000)
someSynchronousOperation()
}
versus
while (isActive) {
someSynchronousOperation()
}
will both respect cancellation. The former does the check in the delay while the latter explicitly checks for cancellation cooperativelySam
03/15/2024, 6:50 PMkevin.cianfarini
03/15/2024, 6:50 PMLukasz Kalnik
03/15/2024, 6:51 PMensureActive() or yield()?Sam
03/15/2024, 6:51 PMLukasz Kalnik
03/15/2024, 6:53 PMLukasz Kalnik
03/15/2024, 6:54 PMLukasz Kalnik
03/15/2024, 6:54 PMdelay() and similar helper functions are "special" in this regard and "good citizens".kevin.cianfarini
03/15/2024, 6:54 PMLukasz Kalnik
03/15/2024, 6:55 PMLukasz Kalnik
03/15/2024, 6:55 PMkevin.cianfarini
03/15/2024, 6:56 PMsuspendCancellableCoroutine 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:
launch(<http://Dispatchers.IO|Dispatchers.IO>) { someBlockingSynchronousOperation() }Sam
03/15/2024, 6:56 PMLukasz Kalnik
03/15/2024, 6:56 PMdelay() with Thread.sleep() the coroutine just runs through:
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)
}Lukasz Kalnik
03/15/2024, 6:57 PMkevin.cianfarini
03/15/2024, 6:57 PMkevin.cianfarini
03/15/2024, 7:00 PMsuspend 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)
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 cancellationLukasz Kalnik
03/15/2024, 7:00 PMval 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.Lukasz Kalnik
03/15/2024, 7:01 PMwhile (isActive) there, maybe it was an ensureActive() check inside the while loop.kevin.cianfarini
03/15/2024, 7:03 PMkevin.cianfarini
03/15/2024, 7:03 PMkevin.cianfarini
03/15/2024, 7:04 PMpollingStopTimeMark.hasPassedNow() should be a loop condition, and not cancel the coroutinekevin.cianfarini
03/15/2024, 7:05 PMSam
03/15/2024, 7:05 PMLukasz Kalnik
03/15/2024, 10:10 PMwithTimeout coroutine function instead of checking the pollingStopTimeMark, and it worked correctly.