本教程介绍了异步编程的不同方法
几十年来,作为开发人员,我们面临着一个需要解决的问题 - 如何防止我们的应用进程被阻塞。无论我们是在开发桌面,移动还是服务器端应用进程,我们都希望避免让用户等待,或者更糟糕的是导致应用进程无法扩展的瓶颈。
有很多方法可以解决这个问题,包括:
- 线程
- 回调
- Futures, Promises 等等
- 响应式扩展
- 协程
线程
到目前为止,线程可能是最常见的避免应用进程阻塞的方法。
12345678910 |
fun (item: Item) { val token = preparePost() val post = submitPost(token, item) processPost(post)} fun preparePost(): Token { return token} |
让我们假设在上面的代码中,preparePost是一个长时间运行的进程,因此会阻塞用户接口。我们可以做的是在一个单独的线程中启动它。这将允许我们避免UI阻塞。这是一种非常常见的技术,但有一系列缺点:
- 线程并不便宜。线程需要昂贵的上下文切换。
- 线程不是无限的。可以启动的线程数受底层操作系统的限制。在服务器端应用进程中,这可能会导致严重的瓶颈。
- 线程并不总是可用。某些平台,例如JavaScript甚至不支持线程
- 线程并不容易。调试线程,避免竞争条件是我们在多线程编程中遇到的常见问题。
回调
使用回调,我们的想法是将一个函数作为参数传递给另一个函数,并在进程完成后调用此函数。
123456789101112 |
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的对象,然后在这个对象上可以操作。
123456789101112131415 |
fun postItem(item: Item) { preparePostAsync() .thenCompose { token -> submitPostAsync(token, item) } .thenAccept { post -> processPost(post) } } fun preparePostAsync(): Promise<Token> { 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使用异步代码的方法是使用协同进程,这是可暂停计算的想法,即一个函数可以在某个时刻暂停执行并稍后恢复的想法。
协同进程的一个好处是,当涉及到开发人员时,编写非阻塞代码与编写阻塞代码基本相同。编程模型本身并没有真正改变。
以下面的代码为例
123456789101112 |
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,这些只是库函数。
有关协同进程和不同可能性的更多信息,请查看参考指南。
原文地址:https://www.cnblogs.com/petewell/p/11422406.html