【问题标题】:Why are kotlin coroutines called asynchronous?为什么kotlin协程叫异步?
【发布时间】:2021-11-12 06:49:25
【问题描述】:

Jetbrains 在每篇关于 kotlin 的文章中都谈到了异步编程。但我不明白为什么它们被称为异步? 据我了解 kotlin 协程 - 这是一个具有预初始化线程池的状态机。我们有一个工作线程池和一个 io 线程池。对我来说,这只是一个多线程编程。如果我们向协程发送阻塞代码,线程将被阻塞。如果我们使用异步方法(来自默认协程库),它会给我们一种异步工作的错觉,但这只不过是将“作业”发送到另一个线程。

另一个问题,如果我们将异步 io 与协程一起使用。但这是 IO API 异步,而不是 kotlin 协程。与其他语言相比,Java 没有好的 io async api(可能是错误的)。据我所知,.NET 已经重建了他们的异步 api(作为 IOCP)以使用 C# 任务,并且.NET 有专用的线程池来等待应用程序的所有 io,因此一个线程可以处理许多 IO 操作。但是 kotlin 协程没有集成到 java nio 中,当我们从协程(有或没有 Dispatcher.IO)调用 nio 时,我们只是要求一个线程等待来自 nio 的数据。 Java NIO 有自己的用于 epoll 或 iocp 的线程池,因此使用 kotlin 协程时,当要求 Dispatcher.IO 给我们一个线程来等待 NIO 的结果时,我们会产生开销,然后 NIO 实现会创建自己的线程(池)来等待数据从插座。我们现在有两个等待,而不是一个线程(池)。

所以协程只是让我们以一种简单的方式将作业发送到另一个线程。如果您的 api 没有使用 kotlin 协程以异步方式实现,则您不能同时使用一个线程执行多项操作。

【问题讨论】:

  • 你真的在这里提出了很多不同的问题——最好在 Kotlin 论坛上讨论这个问题。一般来说,协程让你在执行过程中等待而不阻塞任何线程。是的,如果您使用协程执行阻塞 IO,那么您需要阻塞一些 IO 线程。但是,如果您挂起而不是阻塞,那么不,您不会阻塞任何线程。关于命名,我不会将协程称为异步。事实上,主要目标之一是获得异步编程的性能,但没有异步回调地狱。挂起函数是同步的。
  • 关于 NIO:从未尝试过,但用协程/挂起函数包装异步通道应该是可能的并且非常容易。这样我们可以例如执行 10 个并行 IO 操作,每个操作都将是传统的“阻塞”(实际上是挂起)read()/write() 函数,我们将仅使用单个线程(或更多,无论我们喜欢什么)来执行此操作。 NIO 可能会产生一些内部线程,但我想它会使用其中的一些,它不会创建 10 个线程。这样我们就有 2-4 个线程来处理所有 10 个并行连接。但是再一次 - 我自己没有尝试过。
  • 对您的问题的简单回答是,他们没有像您期望的那样使用异步一词的狭义定义。
  • 我不确定我是否理解正确。是的,如果您的代码主要阻塞 IO,那么在协程中运行它不会给您带来任何(?)性能优势。它不会神奇地使阻塞 IO 变为非阻塞——它只会产生线程。但是,如果您使用协程包装异步 IO,如果您使用为您执行此操作的库,或者您等待 IO 以外的其他东西,例如另一个任务,一些延迟等,那么您可以在不浪费线程的情况下执行此操作,并且仍然保持代码的同步性质。我认为这就是协程的主要内容。
  • 是的,我认为我们在这里错过了一个体面的暂停 IO 库。它可能不会成为标准库的一部分,因为 Kotlin 的 stdlib 必须作为库包含在应用程序中,因此它应该保持苗条。 JetBrains 似乎在做这样的事情 (github.com/Kotlin/kotlinx-io),但它远未准备好。现在我们有其他很好的解决方案,比如Ktor web 框架,它可以完成我所描述的,但适用于 web 应用程序。我们也有一些带有挂起 API 的 3rd 方库,但挂起 API 本身并不能保证线程/资源的最佳使用。

标签: java multithreading kotlin asynchronous


【解决方案1】:

他们谈论异步编程是因为协程(but not limited to)主要作为库出售,以使异步编程更容易(主观)。但是正如您正确指出的那样,协程本身没有任何异步。如果您在协程中执行阻塞代码,它将阻塞底层线程。

但是要理解的一点是,协程只有在与挂起函数结合使用时才真正具有优势,其中线程除了等待结果(回调)之外什么都不做。因此,您可以使用同一个线程再进行十次这样的调用,而不是等待。 另一个主要优点是使用协程编写的异步代码更容易编写和维护。例如,以下是使用回调的异步调用

fun callAPI(){
    getToken{ token ->
        auth(token){ authResult ->
            doSomething(authResult){ finalResult ->
               // use final result
            }
        }
    }
}

这可以简化为使用协程和挂起函数来跟踪

fun callAPI() = scope.launch(){
    val token = getToken()
    val authResult = auth(token)
    val finalResult = doSomething(authResult)
}

现在您可以使用协程来启动多个长时间运行的阻塞任务,但您不会看到任何优势。因为在这种情况下,协程只不过是线程之上的无用抽象。

【讨论】:

    【解决方案2】:

    我会尝试从不同的角度看待问题。 协程基于挂起函数。挂起函数是异步的。

    协程是状态机这一事实在此无关紧要。状态机可以转换为异步函数链,反之亦然。

    不应将调度程序视为“线程池”,而应将它们视为反应器模式或事件循环。是的,您可以阻止事件循环。但是,这并不意味着事件循环不是异步的,或者您应该这样做。

    只有在您不使用挂起函数的情况下,您所说的“异步作业的错觉”才是“错觉”。由于 Kotlin 代码的很大一部分是挂起的(流、Ktor 等),实际上,您的大部分代码实际上都是异步的。

    Java 的 NIO 与 Kotlin 有不同的抽象:回调。可以使用suspendCancellableCoroutine 将这些转换为协程

    最后一部分大多是错误的:

    协程只是让我们以一种简单的方式将作业发送到另一个线程。如果您的 api 没有使用 kotlin 协程以异步方式实现,则您不能同时使用一个线程执行多项操作。

    单个 Dispatcher 线程将在多个协程之间进行上下文切换,除非其中一个协程阻塞:不使用任何挂起函数并执行 IO 或 CPU 密集型任务。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-01-15
      • 2019-04-06
      • 2022-01-18
      • 1970-01-01
      • 2018-02-22
      • 2019-02-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多