5.12.1 异步编程技术
5 分钟阅读
5.12.1 异步编程技术
几十年来,作为开发者,我们一直要面对一个问题:如何避免应用阻塞。无论我们开发的是桌面、移动还是后端应用,都不希望让用户等待,更不希望制造阻碍应用扩展的瓶颈。
解决这个问题有许多方式,包括:
在解释什么是协程之前,我们先简要回顾其他几种解决方案。
线程
线程大概是避免应用阻塞最为人熟知的方式。
| |
假设上面的代码中 preparePost 是一个长时间运行的过程,因此会阻塞用户界面。我们可以把它放到单独的线程中启动,这样就避免了 UI 阻塞。这是一项非常常见的技术,但它有一系列缺点:
- 线程并不廉价。线程需要上下文切换,而这是有代价的。
- 线程不是无限的。可启动的线程数量受底层操作系统限制。在后端应用中,这可能造成严重的瓶颈。
- 线程并非总是可用。有些平台(例如 JavaScript)甚至不支持线程。
- 线程并不简单。调试线程、避免竞态条件,是我们在多线程编程中经常遇到的难题。
回调
回调的思路是把一个函数作为参数传给另一个函数,并在处理完成后调用它。
| |
这在原理上似乎是一种更优雅的解决方案,但同样存在几个问题:
- 嵌套回调难以处理。通常,作为回调使用的函数往往又需要自己的回调。这会形成一连串嵌套回调,导致代码难以理解。这种模式常被称为回调地狱,或者厄运金字塔,因为这种深层嵌套回调产生的缩进会形成三角形。
- 错误处理很复杂。嵌套模型让错误处理和传播变得有些复杂。
回调在 JavaScript 这类事件循环架构中相当常见,但即便在那里,人们也普遍转向了 promises 或响应式扩展等其他方式。
Futures、promises 及其他
futures 或 promises(不同语言或平台可能使用其他术语)背后的想法是:当我们发起调用时,就被承诺在某个时刻该调用会返回一个 Promise 对象,然后我们可以对它进行操作。
| |
这种方式要求我们在编程方式上做出一系列改变,特别是:
- 不同的编程模型。与回调类似,编程模型从自上而下的命令式方式转向带有链式调用的组合式模型。循环、异常处理等传统程序结构在这种模型中通常不再适用。
- 不同的 API。通常需要学习全新的 API,例如
thenCompose或thenAccept,而且这些 API 在不同平台上也可能不同。 - 特定的返回类型。返回类型不再是我们真正需要的数据,而是一个需要进一步检视的新类型
Promise。 - 错误处理可能很复杂。错误的传播和链式处理并不总是那么直接。
响应式扩展
响应式扩展(Rx)由 Erik Meijer 引入 C#。虽然它确实在 .NET 平台上被使用,但真正进入主流是在 Netflix 把它移植到 Java 并命名为 RxJava 之后。此后,各种平台都出现了大量移植版本,包括 JavaScript(RxJS)。
Rx 背后的想法是走向所谓的 observable streams(可观察流):我们把数据视为流(无限量的数据),而这些流可以被观察。就实际应用而言,Rx 其实就是观察者模式加上一系列允许我们对数据进行操作的扩展。
在思路上它与 Futures 颇为相似,不过可以把 Future 看作返回一个离散元素,而 Rx 返回的是流。然而与前面几种方式一样,它也引入了一套全新的编程模型思维方式,其著名表述是:
“一切皆是流,并且可被观察”
这意味着要用不同的方式看待问题,与我们在编写同步代码时的习惯有相当大的转变。与 Futures 相比的一个好处是,由于它被移植到众多平台,无论你使用 C#、Java、JavaScript 还是任何可用 Rx 的语言,通常都能获得一致的 API 体验。
此外,Rx 确实在错误处理方面提供了相对更好的方式。
协程
Kotlin 处理异步代码的方式是使用协程,即"可挂起的计算"这一理念,也就是说函数可以在某个时刻挂起其执行,稍后再恢复。
不过,协程的好处之一是:对开发者而言,编写非阻塞代码与编写阻塞代码基本上没有区别。编程模型本身并不会真正改变。
例如下面这段代码:
| |
这段代码会启动一个长时间运行的操作,而不会阻塞主线程。preparePost 就是所谓的 suspendable function(可挂起函数),因此前缀了 suspend 关键字。如上所述,这意味着该函数会执行、暂停执行,并在某个时刻恢复。
- 函数签名保持完全不变,唯一的区别是加上了
suspend。不过返回类型仍然是我们希望返回的类型。 - 代码仍然像编写同步代码那样自上而下书写,除了使用一个名为
launch的函数(用于启动协程,详见其他教程)之外,不需要任何特殊语法。 - 编程模型和 API 保持不变。我们可以继续使用循环、异常处理等,无需学习一整套新 API。
- 它与平台无关。无论目标平台是 JVM、JavaScript 还是其他平台,我们写的代码都一样。在底层,编译器负责把它适配到各个平台。
协程并不是新概念,更不是 Kotlin 发明的。它已经存在了几十年,并在 Go 等其他一些编程语言中很流行。不过需要强调的是,在 Kotlin 中的实现方式下,大部分功能都委托给了库。事实上,除了 suspend 关键字之外,语言没有添加其他关键字。这与 C# 等把 async 和 await 作为语法一部分的语言有所不同。在 Kotlin 中,它们只是库函数。
更多信息请参阅协程参考。