【问题标题】: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() 或类似的东西)。

    如果是这样的话,我在这里主要看到两种情况:

    1. 库知道这是一个长时间运行的操作并支持线程中断来取消它。 Thread.sleep 和一些 I/O 操作就是这种情况。
    2. 库函数确实在整个操作过程中阻塞了调用线程,并不是为了处理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 如果任务不支持线程中断。

      所以最重要的是要么接受这个可能挂起的长东西,要么要求该库的开发人员处理中断或使任务可取消。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-02
      • 1970-01-01
      • 2021-10-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-04
      • 2021-11-30
      • 2021-02-01
      相关资源
      最近更新 更多