I need to wrap some Java-callback function using timeout. Callback may be never called, so it should be interrupted with exception. Here was my first try:
fun main() = runBlocking {
withTimeout(500) {
async {
notCalledCallback()
}.await()
}
Unit
}
private suspend fun notCalledCallback() = suspendCoroutine<Boolean> { cont ->
startScanning(object : SomeCallback {
override fun done() {
cont.resume(true)
}
})
}
fun startScanning(callBack: SomeCallback) {
// callback may never be invoked
// callBack.done()
}
interface SomeCallback {
fun done()
}
I expected to have a TimeoutCancellationException
after 500ms, but actually it never happens. However if I replace
async {
notCalledCallback()
}.await()
with
GlobalScope.async {
notCalledCallback()
}.await()
it starts to work. Why? What is the difference between async
and GlobalScope.async
in this case and why it works in latter case?
CodePudding user response:
while (true) {
Thread.sleep(1)
}
This block of code does not comply with coroutine practices and doesn't offer the coroutine framework any opportunity to cancel it.
A correct implementation of infinityFunction()
would be to simply call awaitCancellation
. Alternately, you could replace Thread.sleep
with delay
.
Notably, using GlobalScope
actually breaks the correct relationship between your coroutines (making the async
block not a child of the calling coroutine), with the result that your main
function doesn't wait for infinityFunction()
to properly finish cancelling. While this appears to make your code work, it actually just conceals a worse bug.
CodePudding user response:
The answer is actually very simple: suspendCoroutine()
is not cancellable. You need to instead use a very similar function: suspendCancellableCoroutine().
Please be aware that ideally you should not only swap one function with another, but also properly cancel the asynchronous operation before resuming the coroutine. Otherwise you leak this background operation as it is entirely detached from your execution context. You can detect cancellations with cont.invokeOnCancellation()
, as described in the documentation linked above.
If you use GlobalScope
then you await()
for the operation in your current execution context, but the operation itself runs in another context. In this case if you cancel, then you cancel waiting, but you don't cancel the operation and you don't care whether it completes or not.