抬头仰望星空,是否能发现自己的渺小。

伪斜杠青年

人们总是混淆了欲望和理想

Kotlin 协程:suspendCoroutine withTimeout 无效问题,我也把它皮扒了!!!

试了挺久,到底还是对协程理解得不够深刻。

想来我又把公司坑了。。。

之前以为的写法(不生效):

withTimeout(timeout){
    suspendCoroutine { 
        do sth
    }
}

后来看 stackoverflow 上的高赞改为:

suspend inline fun <T> suspendCoroutineWithTimeout(
timeout: Long, crossinline block: (Continuation<T>) -> Unit
) = withTimeout(timeout) {
suspendCancellableCoroutine(block = block)
}

我寻思,这特么不是一样的么? 是不一样的,一个可Cancellable 一个不可

private suspend inline fun testTimeOut() {
    withContext(Dispatchers.IO) {
        kotlin.runCatching {
            //not work
            /*withTimeout(2000L) {
                    suspendCoroutine<String> {
                        it.resume(time().get())
                    }
            }*/            
            //work
            withTimeout(2000L) {
                GlobalScope.launch {
                    suspendCoroutine<String> {
                        it.resume(time().get())
                    }
                }.join()
            }

            //work
            val d = async {
                suspendCoroutine<String> {
                    it.resume(time().get())
                }
            }
            withTimeout(2000) { d.await() }
        }.onFailure {
            it.printStackTrace()
        }
    }
}
class time {
fun get(): String {
for (i in 0..1000) {
Thread.sleep(1000)
}
return ""
}
}

至于为什么不生效,我起初的理解:asyn 和 join 都返回了一个可取消可挂起的 future,这样做到可感知?看了下源码:

join 最终对应的是:

await 最终对应的是:

以上两个方法都在 JobSupport 类中。

普通 suspendCoroutine:

靠,根本看不出什么区别!!!关键在于Cancellable。于是我又偷偷的把【码上开学】看了一遍,扔老师说过了,协程没什么特别的,就是几个线程切来切去,非阻塞式挂起其实就是因为可以使用挂起函数配合一些如 withContext 进行按行切线程,从而不阻塞线程,不要想太复杂。

主要原因:

suspendCoroutine 将当前协程阻塞了,withTimeOut 无法执行完成,为什么会阻塞?因为线程并没有切换,单线程当然会阻塞,这又是一个很简单的问题!!!

而其他两种办法,都是在外层再套了一层协程,所以其阻塞的都是 async 或者GlobalScope 启动的新协程,这样 withTimeOut 才可以继续对其进行超时的判断。

(PS: withTimeOut 内部本身就是使用的 delay )

不信? 打个日志看一看:

第一种情况:

Log.d(TAG, "testTimeOut start: ${Thread.currentThread()}")
withTimeout(2000L) {
suspendCoroutine<String> {
Log.d(TAG, "testTimeOut: before resume ${Thread.currentThread()}")
it.resume(time().get())
}
}

第二种情况:

Log.d(TAG, "testTimeOut start: ${Thread.currentThread()}")
withTimeout(2000L) {
GlobalScope.launch {
suspendCoroutine<String> {
Log.d(TAG, "testTimeOut: before resume ${Thread.currentThread()}")
it.resume(time().get())
}
}.join()
}

第三种情况:

Log.d(TAG, "testTimeOut start: ${Thread.currentThread()}")
val d = async {
suspendCoroutine<String> {
Log.d(TAG, "testTimeOut: before resume ${Thread.currentThread()}")
it.resume(time().get())
}
}
withTimeout(2000) { d.await() }

豁然开朗,所以最后 挂起的协程 超时的正解是:

withTimeout(2000L) {
GlobalScope.launch {
suspendCoroutine<String> {
it.resume(time().get())
}
}.join()
}

以及:

val d = async {
    suspendCoroutine<String> {
        it.resume(time().get())
    }
}
withTimeout(2000) { d.await() }

以及最好的办法是:

suspend inline fun <T> suspendCoroutineWithTimeout(
        timeout: Long, crossinline block: (Continuation<T>) -> Unit
) = withTimeout(timeout) {
    suspendCancellableCoroutine(block = block)
}

写完已经凌晨1点半啦。。。学艺不精(菜)便是如此。

注:主要讨论withTimeOut中是否是同步的情况,一般 suspendCoroutine 会配合异步代码块使用,这种情况是没问题的。后来基本没用过~

修正于:2022.4 感谢评论区小伙伴。

参考:

码上开学:https://kaixue.io/

kotlin-coroutine-withtimeout-does-not-cancel-when-using-withcontext-to-get-non-b

kotlin-coroutines-with-time


本站由以下主机服务商提供服务支持:

4条评论

  • 嘿嘿抛物线

    stackoverflow 上的suspendCancellableCoroutine有效, suspendCoroutine因为是不可取消的,所以无效。withTimeout的方法注释中有写“cancellable suspending function”

  • dylan

    “suspendCoroutine 将当前协程阻塞了,withTimeOut 根本无法执行”,这句话不对!!!
    // withTimeout(timeout){
    // suspendCoroutine {
    // do something
    // }
    // }
    suspendCoroutine确实把当前协程阻塞了,从而使withTimeOut也被阻塞才对,withTimeOut肯定执行了,因为suspendCoroutine就是执行在withTimeOut的协程体内。
    另外suspendCancellableCoroutine这用法应该是没问题的,不知道你为啥说这种写法不生效,你可以try { withTimeOut 代码块}catch{} finally,肯定是能捕获到超时异常的

    • Mosaic-C

      你说的是对的,楼上也说过了,过去太久了没去修正,我修正下,感谢提醒。

发表评论