什么机制导致 Kotlin 协程挂起?

What mechanism causes a Kotlin coroutine to suspend?

我正在尝试理解 kotlin 协程,我来自 C#,但在 kotlin 中有些东西我不理解。在这种情况下,我正在 Quarkus 框架中使用 Kotlin 编写一个 webapi。据我所知,如果我将控制器(或资源)函数标记为挂起函数,quarkus 将自动在协程中启动它。

我遇到的问题是我不知道暂停协程的首选方法是什么。我在 kotlin 协程上看到的绝大多数示例都使用 delay() 函数,它在内部使用 suspendCancellableCoroutine() 来暂停函数。这是有道理的,但我没有看到很多显式调用 suspendCancellableCoroutine() 的示例。我已经阅读了一些关于在挂起函数中生成的底层代码的阅读资料,一些资源让我相信,通过调用另一个挂起函数,我将达到一个挂起点,这将挂起我的协程。在 C# 中,我通常只是从异步函数内部调用 await() 来执行长 运行ning 代码。

在我的 kotlin 设置中,我设置了一个 jmeter 实例,我模拟 5 个线程同时调用我的 API,同时将我的程序限制在 quarkus 中的单个线程上 运行。我的 API 然后调用另一个 API(从现在开始我会调用它 API,数据 API),这可能是一个很长的 运行宁操作。出于测试目的,我的数据 API 有 1 秒的休眠时间。

本质上:

web api controller -> web api processing -> web api calls data api through client -> data API does slow operation

我试过在调用数据 API 时调用 async/await,这似乎有效,JMeter 报告说 5 个请求在大约 1 秒内全部完成,并且我有日志记录表示所有 5 个请求都在单个线程上处理。不过这感觉很笨拙。我已经在协程中了,现在我的协程正在创建一个新的协程(async 是一个协程构建器)来执行长 运行ning 函数。

我还删除了 async/await 并将对数据 API 的调用更新为一个挂起函数(尽管这是从 resteasy 客户端生成的客户端)。这似乎也有效,但 resteasy reactive 可能会生成一些正在为我暂停的东西。我需要使用一个更简单的示例,但与此同时...

如果我没有在 Kotlin 中使用 delay() 函数,并且我正在协程中执行代码,那么指示一段代码可能会阻塞并且我的协程应该暂停的首选方法是什么?我要启动一个新的协程吗?调用 suspendCancellableCoroutine()?或者是其他东西?可能想多了,但我想确保我理解这一点。

协程库提供了几个挂起函数,您可以使用它们在协程或另一个挂起函数中挂起,其中:

  • withContext
  • delay
  • coroutineScope
  • supervisorScope
  • suspendCoroutine
  • suspendCancellableCoroutine
  • Job.join
  • Deferred.await

将阻塞(long-running 同步)代码转换为可以在协程中使用的代码的典型方法是将其包装在 withContext(Dispatchers.Default) { }withContext(Dispatchers.IO) { } 中。如果是你重复使用的东西,你可以为它写一个挂起函数:

suspend fun foo() = withContext(Dispatchers.IO) {
    blockingFoo()
}

但如果是一些 one-off 阻塞代码块,您可以直接在协程中使用 withContext

注意,使用 async { }.await() 基本上是从来没有做过的。编译器会警告您不要这样做。您应该改用 withContext。当一个协程需要来自已传递给它的其他协程的结果时,或者当您在 coroutineScope 块中使用多个并行子协程时,将使用在 Deferred 上调用 await

将异步 callback-based 代码转换为挂起函数以便在协程中同步使用它的典型方法是使用 suspendCoroutinesuspendCancellableCoroutine。您可以查看如何使用它们。他们的水平很低。许多库(如 Retrofit 和 Firebase)已经提供了挂起函数,您可以使用它来代替回调。

coroutineScopesupervisorScope 用于在您的协程中创建一个范围,以便 运行 多个子协程并行并等待它们。