这2天在跟着kotlin的官方文档练习阅读,复习协程.看到Cancellation and timeouts的时候,觉得自己之前的超时处理逻辑如果切换成withTimeout
将会更加简洁.果然官方教程是最好的教程.先看看我之前的实现:
val timeOut = lifecycleScope.async {
delay(40 * 1000)
LogUtils.e(TAG, "上传超时 onTimeout: 45s")
false
}
val uploadResult = lifecycleScope.async {
suspendCoroutine<Boolean> {
upload(file,file.name){ key, info, response ->
if (info?.isOK == true) {
LogUtils.d(TAG, "上传成功: ${response.toString()}")
it.resume(true)
} else {
LogUtils.e(TAG, "上传失败:code=${info.statusCode} error = ${info.error}")
it.resume(false)
}
}
}
}
if (select {
timeOut.onAwait { it }
uploadResult.onAwait { it }
}) {//前面2个async哪一个先返回就取哪一个的值
//上传成功
timeOut.cancel()
uploadAndDelete(item)
}else{
//超时或失败
timeOut.cancel()
uploadResult.cancel()
}
LogUtils.v(TAG, "onComplete")
这样当然是可以达到目的的,但是感觉不够简洁.也没有实际用过withTimeout
,结果一番改造后,超时失效了,测试的代码也翻了车,改了好几个版本都不行,先来看看最后一版测试的代码:
fun main() = runBlocking {
launch {
val succ = withTimeoutOrNull(3000){
//delay(4000)
val result = suspendCoroutine<Boolean> {
//模拟进行上传操作一直不调用resume,触发超时
//it.resume(true)
}
println("result $result")
return@withTimeoutOrNull result
}
if (succ == true) {
println("上传成功 删除文件")
} else {
println("上传超时")
}
}
println("onComplete")
}
---------------------运行输出---------------------
> Task :MainKt.main()
onComplete
结果就输出了onComplete然后无尽挂起了,也不返回超时,程序也不结束.但是放开delay(4000)
手动延迟后,又返回超时了,百思不得其解.一度对自己的使用姿势产生了怀疑.经过使用async{}
,whitContext{}
等等wrap,awite()
,jion()
等等调用尝试后,终于发现了问题所在:
前面使用了suspendCoroutine
来将一个api的回调转换成协程.之前那么用是没有问题的,但是现在被withTimeoutOrNull
包裹使用的话,就会有问题了,因为 suspendCoroutine
是不可取消的 .有点像被withContext(NonCancellable) {...}
包裹的代码对其调用cancle是无效的.详见:Run non-cancellable block.其实只要对比一下suspendCoroutine
和suspendCancellableCoroutine
的代码就能发现一些端倪:
@SinceKotlin("1.3")
@InlineOnly
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())//这里创建了一个Continuation的实现类
block(safe)
safe.getOrThrow()
}
}
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)//这里创建了Continuation的另外一个实现类,从类名和这个类的签名可知,这是可取消的
/*
* For non-atomic cancellation we setup parent-child relationship immediately
* in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
* properly supports cancellation.
*/
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
因为suspendCoroutine
的实现不支持取消,所以即使超时了,这个挂起点也将一直处于挂起状态,后续的代码自然就得不到执行了.找到了问题修改suspendCoroutine
为suspendCancellableCoroutine
问题就得到了解决,最终的实现是:
lifecycleScope.launch {
val success = withTimeoutOrNull(40*1000){
return@withTimeoutOrNull suspendCancellableCoroutine<Boolean> {
upload(file,file.name){ key, info, response ->
if (info?.isOK == true) {
LogUtils.d(TAG, "上传成功: ${response.toString()}")
it.resume(true)
} else {
LogUtils.e(TAG, "上传失败:code=${info.statusCode} error = ${info.error}")
it.resume(false)
}
}
}
when(success){
null->LogUtils.e(TAG, "上传超时 onTimeout: 40s")
true->uploadAndDelete(item)
}
LogUtils.v(TAG, "onComplete")
}
这里使用withTimeoutOrNull
比withTimeout
更加的符合我的需要,它们的区别是前者在超时时将返回null,后者将抛出异常,需要try{}catch
.