(译)异步编程技术

原文链接

本教程介绍了异步编程的不同方法

几十年来,作为开发人员,我们面临着一个需要解决的问题 - 如何防止我们的应用程序被阻塞。无论我们是在开发桌面,移动还是服务器端应用程序,我们都希望避免让用户等待,或者更糟糕的是导致应用程序无法扩展的瓶颈。

有很多方法可以解决这个问题,包括:

  • 线程
  • 回调
  • Futures, Promises 等等
  • 响应式扩展
  • 协程

线程

到目前为止,线程可能是最常见的避免应用程序阻塞的方法。

fun postItem(item: Item) {
    val token = preparePost()
    val post = submitPost(token, item)
    processPost(post)
}

fun preparePost(): Token {
    // makes a request and consequently blocks the main thread
    return token
}

让我们假设在上面的代码中,preparePost是一个长时间运行的进程,因此会阻塞用户界面。我们可以做的是在一个单独的线程中启动它。这将允许我们避免UI阻塞。这是一种非常常见的技术,但有一系列缺点:

  • 线程并不便宜。线程需要昂贵的上下文切换。
  • 线程不是无限的。可以启动的线程数受底层操作系统的限制。在服务器端应用程序中,这可能会导致严重的瓶颈。
  • 线程并不总是可用。某些平台,例如JavaScript甚至不支持线程
  • 线程并不容易。调试线程,避免竞争条件是我们在多线程编程中遇到的常见问题。

回调

使用回调,我们的想法是将一个函数作为参数传递给另一个函数,并在进程完成后调用此函数。

fun postItem(item: Item) {
    preparePostAsync { token -> 
        submitPostAsync(token, item) { post -> 
            processPost(post)
        }
    }
}

fun preparePostAsync(callback: (Token) -> Unit) {
    // make request and return immediately 
    // arrange callback to be invoked later
}

原则上这感觉就像一个更优雅的解决方案,但又有几个问题:

  • 嵌套回调的难度。通常,用作回调的函数通常最终需要自己的回调。这导致一系列嵌套回调,导致难以理解的代码。该模式通常被称为标题圣诞树(大括号代表树的分支)。
  • 错误处理很复杂。嵌套模型使错误处理和传播变得更加复杂。

回调在诸如 JavaScript 之类的事件循环体系结构中很常见,但即使在那里,通常人们已经转而使用其他方法,例如promises或响应式扩展。

Futures,Promises 等等

Future 或Promise背后的思想(也可以根据语言/平台提及其他术语)是,当我们调用的时候,我们承诺在某些时候它会返回一个名为Promise的对象,然后在这个对象上可以操作。

fun postItem(item: Item) {
    preparePostAsync() 
        .thenCompose { token -> 
            submitPostAsync(token, item)
        }
        .thenAccept { post -> 
            processPost(post)
        }
         
}

fun preparePostAsync(): Promise<Token> {
    // makes request an returns a promise that is completed later
    return promise 
}

这种方法需要对我们的编程方式进行一系列更改,尤其是

  • 不同的编程模型。与回调类似,编程模型从自上而下的命令式方法转变为具有链式调用的组合模型。传统的程序结构(如循环,异常处理等)通常在此模型中不再有效。
  • 不同的API。通常需要学习一个全新的API,例如thenCompose或thenAccept,它们也可能因平台而异。
  • 特定的返回类型。返回类型不再是我们需要的实际数据,而是返回一个必须被内省的新类型Promise。
  • 错误处理可能很复杂。错误的传播和链接并不总是直截了当的。

响应式扩展

Reactive Extensions(Rx)由Erik Meijer引入C#。虽然它确实在.NET平台上使用,但它确实没有达到主流采用,直到Netflix将其移植到Java,命名为RxJava。从那时起,已经为各种平台提供了许多端口,包括JavaScript(RxJS)。

Rx背后的想法是转向所谓的可观察流,我们现在将数据视为流(无限量的数据),并且可以观察到这些流。实际上,Rx只是具有一系列扩展的观察者模式,它允许我们对数据进行操作。

在方法中它与 Futures 非常相似,但是人们可以将 Future 视为返回离散元素,从而Rx返回一个流。然而,与之前的类似,它还引入了一种全新的思考我们的编程模型的方式,着名的是

“一切都是流,它是可观察的”

这意味着处理问题的方式不同,并且在编写同步代码时从我们使用的方式发生了相当大的转变。与Futures相反的一个好处是,鉴于它被移植到如此众多的平台,通常我们可以找到一致的API体验,无论我们使用它,无论是C#,Java,JavaScript还是Rx可用的任何其他语言。

此外,Rx确实引入了一种更好的错误处理方法。

协程

kotlin使用异步代码的方法是使用协同程序,这是可暂停计算的想法,即一个函数可以在某个时刻暂停执行并稍后恢复的想法。

协同程序的一个好处是,当涉及到开发人员时,编写非阻塞代码与编写阻塞代码基本相同。编程模型本身并没有真正改变。

以下面的代码为例

fun postItem(item: Item) {
    launch {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
}

suspend fun preparePost(): Token {
    // makes a request and suspends the coroutine
    return suspendCoroutine { /* ... */ } 
}

此代码将启动长时间运行的操作,而不会阻塞主线程。 preparePost是所谓的可挂起函数,因此关键字 suspend 为它添加前缀。如上所述,这意味着该功能将在某个时间点执行,暂停执行和恢复。

  • 功能签名保持完全相同。唯一的区别是暂停被添加到它。但返回类型是我们想要返回的类型。
  • 代码仍然被编写为好像我们正在编写同步代码,自上而下,不需要任何特殊语法,除了使用一个名为 launch 的函数,它基本上启动了协同程序(在其他教程中介绍)。 *编程模型和API保持不变。我们可以继续使用循环,异常处理等,而且不需要学习一整套新的API
  • 它与平台无关。无论我们是针对JVM,JavaScript 还是其他任何平台,我们编写的代码都是相同的。表现之下,编译器负责将其适应每个平台。
  • 协同程序不是一个新概念,更不用说是由 Kotlin 发明了。它们已经存在了几十年,并且在Go等其他编程语言中很受欢迎。值得注意的是,它们在 Kotlin 中的实现方式,大部分功能都委托给了库。事实上,除了 suspend 关键字之外,该语言中没有添加其他关键字。这与C#之类的语言略有不同,这些语言具有异步并等待语法的一部分。使用 Kotlin,这些只是库函数。

有关协同程序和不同可能性的更多信息,请查看参考指南。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章