【问题标题】:How to cancel kotlin coroutine with potentially "un-cancellable" method call inside it?如何取消 kotlin 协同程序,其中可能有“不可取消的”方法调用?
【发布时间】:2023-02-03 00:34:43
【问题描述】:
我有这段代码:
// this method is used to evaluate the input string, and it returns evaluation result in string format
fun process(input: String): String {
val timeoutMillis = 5000L
val page = browser.newPage()
try {
val result = runBlocking {
withTimeout(timeoutMillis) {
val result = page.evaluate(input).toString()
return@withTimeout result
}
}
return result
} catch (playwrightException: PlaywrightException) {
return "Could not parse template! '${playwrightException.localizedMessage}'"
} catch (timeoutException: TimeoutCancellationException) {
return "Could not parse template! (timeout)"
} finally {
page.close()
}
}
如果该方法执行时间过长(例如:输入可能包含无限循环),它应该在 5 秒后抛出异常,但它确实存在(我认为会变成死锁),因为协程应该是合作的。但是我调用的方法来自另一个库,我无法控制它的计算(为了坚持 yield() 或类似的东西)。
所以问题是:是否有可能让这样的协程超时?如果是,那么如何?
我应该使用 java thread insted 并在一段时间后将其杀死吗?
【问题讨论】:
标签:
kotlin
kotlin-coroutines
playwright
【解决方案1】:
但是我调用的方法来自另一个库,我无法控制它的计算(为了坚持 yield() 或类似的东西)。
如果是这样的话,我在这里主要看到两种情况:
- 库知道这是一个长时间运行的操作并支持线程中断来取消它。
Thread.sleep 和一些 I/O 操作就是这种情况。
- 库函数确实在整个操作过程中阻塞了调用线程,并不是为了处理thread interrupts而设计的
情况一:库函数可中断
如果你幸运地处于情况 1,那么只需将库的调用包装到 runInterruptible 块中,协程库就会将取消转换为线程中断:
fun main() {
runBlocking {
val elapsed = measureTimeMillis {
withTimeoutOrNull(100.milliseconds) {
runInterruptible {
interruptibleBlockingCall()
}
}
}
println("Done in ${elapsed}ms")
}
}
private fun interruptibleBlockingCall() {
Thread.sleep(3000)
}
情况 2:库函数不可中断
在更有可能的情况 2 中,您有点不走运。
我应该使用 java thread insted 并在一段时间后将其杀死吗?
Java 中没有“杀死一个线程”这样的东西。见Why is Thread.stop deprecated?,或How do you kill a Thread in Java?。
简而言之,在这种情况下,您别无选择,只能阻止一些线。
也就是说,它不一定是你的线。你可以技术上在另一个线程上启动一个单独的协程(如果您的线程是单线程的,则使用另一个调度程序),以包装库函数调用,然后 join() withTimeout 中的作业以避免永远等待它。然而,这可能很糟糕,因为您基本上是将问题推迟到用于启动不可取消任务的任何范围(这实际上是我们不能在此处使用简单的 withContext 的原因)。
如果您使用 GlobalScope 或另一个长时间运行的作用域,您实际上会泄漏挂起的协程(不知道会持续多长时间)。
如果您使用更本地的父范围,则可以将问题推迟到该范围。例如,如果您使用封闭的 runBlocking 的范围(如您的示例),就会出现这种情况,这使得该解决方案毫无意义:
fun main() {
val elapsed = measureTimeMillis {
doStuff()
}
println("Completely done in ${elapsed}ms")
}
private fun doStuff() {
runBlocking {
val nonCancellableJob = launch(Dispatchers.IO) {
uncancellableBlockingCall()
}
val elapsed = measureTimeMillis {
withTimeoutOrNull(100.milliseconds) {
nonCancellableJob.join()
}
}
println("Done waiting in ${elapsed}ms")
} // /! runBlocking will still wait here for the uncancellable child coroutine
}
// Thread.sleep is in fact interruptible but let's assume it's not for the sake of the example
private fun uncancellableBlockingCall() {
Thread.sleep(3000)
}
输出类似:
Done waiting in 122ms
Completely done in 3055ms
我不知道不泄漏资源的解决方案。 Using an ExecutorService would suffer from the same problem 如果任务不支持线程中断。
所以最重要的是要么接受这个可能挂起的长东西,要么要求该库的开发人员处理中断或使任务可取消。