Kotlin
协程
取消和超时

取消和超时

通过launch返回的Job,可以对所创建的协程进行操作

fun main() = runBlocking {
    val job = launch {
        repeat(1000) {
            println("coroutine: I'm sleeping $it")
            delay(500L)
        }
    }
    delay(1300L)
    println("I'm tried to waiting")
    job.cancel() // 取消协程
    job.join() // 等待协程运行完毕
    println("I'm quiting")
}

一旦main中调用了cancel,协程将会被取消,可以使用cancelAndJoin来代替cancel+join

取消是合作性的

协程的取消是合作性的,协程所有的代码必须配合才能取消,协程中所有的挂起函数都是可以被取消的,它们将检查协程的取消情况,并在取消时抛出CancellationException异常。但如果协程在计算中,且不检查取消,在其内部无可被取消的函数时,它将无法被取消

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        println("coroutine: I'm sleeping")
        waitForSeconds(10)
        println("coroutine: I'm wake")
    }
    delay(1300L)
    println("main:I'm waiting")
    job.cancelAndJoin()
    println("main: I'm exiting")
}
 
fun waitForSeconds(value: Long) {
    val previousTime = System.currentTimeMillis()
    while (System.currentTimeMillis() < previousTime + (value * 1000));
}

在这段代码中,即使协程被取消了,但是它仍然会继续运行

使用try..catch捕获这个异常也会有这种效果

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        repeat(5) {
            println("coroutine: $it")
            try {
                delay(1000L)
            } catch (e: CancellationException) {
                println(e)
            }
        }
    }
    delay(1500L)
    println("main is waiting...")
    job.cancelAndJoin()
    println("main is exiting...")
}

但这种try..catch是一种反模式

使计算代码可以被取消

可以通过yeild函数或自行判断使得这种计算代码能够随着协程的取消而结束

while (isActive) {
    if (System.currentTimeMillis() >= previousTime) {
        println("coroutine: I'm sleeping")
        previousTime+=2000L
    }
}

使用finally释放资源

因为中断函数在被取消时将会抛出异常,所以可以通过try..catch或者use的方式释放资源

val job = launch(Dispatchers.Default) {
    try {
        repeat(1000) {
            println("coroutine is sleeping... $it")
            delay(500L)
        }
    } finally {
        println("coroutine is running finally")
    }
}

运行不可取消的块

使用withContext(NonCancellable)的方式,能够在已被取消的协程中仍然挂起

val job = launch {
    try {
        repeat(1000) {
            println("coroutine is sleeping... $it")
            delay(500L)
        }
    } finally {
        withContext(NonCancellable) {
            println("I'm running finally")
            delay(10000L)
            println("coroutine is finished...")
        }
    }
}

超时

可以使用withTimeout为一个协程设置超时时间,在协程运行时间超时时,将会抛出TimeoutCancellationException异常

同样的,可以使用try..catch来处理资源释放,或者使用withTimeoutOrNull来默认返回一个空值

val result = withTimeoutOrNull(1000L) {
    delay(1200L)
}
println(result)

异步超时和资源

withTimeout中的超时事件相对于其块中运行的代码是异步的,并且可能随时发生,在块内申请的资源需要在块外关闭或者释放

如果在withTimeout中触发了超时,而资源没有关闭,可能会导致资源泄漏。这时,一般将释放资源的方法写在withTimeout块外

var acquired = 0
 
class MyResource : Closeable {
    init { acquired++ }
 
    override fun close() { acquired-- }
}
 
fun main() {
    runBlocking {
        repeat(10_000) {
            launch {
                var resource: MyResource? = null
                try {
                    withTimeout(60) {
                        delay(50)
                        resource = MyResource()
                    }
                } finally {
                    resource?.close()
                }
            }
        }
    }
    println(acquired)
}

值得注意的是,此处的acquired++acquired--因为在同一个runBlocking块中,它们是线程安全的