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.