跳至主要內容

异步/等待模式

guodongAndroid大约 8 分钟

前言

在计算中最大的问题之一是能够从异步函数返回值。更大的问题是,如果您调用的函数创建了一个不同的线程来运行,您不能将值返回给外部函数。这是一个程序限制,因为系统不知道何时返回并且很难在线程之间进行桥接。但是可以使用 异步/等待 模式实现这种行为。

这是一个非常简单的想法:把您需要的值构建一个包装器,然后调用一个提供该值的函数并将其传递给包装器。准备好后,您可以请求它。它将一直回到队列,因为包装器也可能是一个简单的类,它包含一个容量的队列。

一旦您请求了这个值,就暂停了您请求它的函数,直到数据返回。这种机制是有效的,并且经过了时间的考验。在 Java API 中有一个非常相似的实现 —— future。但是,如果您使用 Javascript,您可能熟悉采用类似方法但执行方式不同的 promise

回顾过去

有时,最好仔细回顾过去,看看您学到了什么,也许可以用来实现您的目标。在设计协程时,设计 API 的团队就是这样做的。这并不奇怪,因为协程的概念已经有几十年的历史了。进一步说,他们参考了 futurepromise 模式。每个模式都有处理异步并提供值的特定语法和方式。让我们看看它们到底是什么。

Promising Values

一个 promise 结构就是名称所声明的 —— 一个值的 promise,它可能存在也可能根本不存在。promised 的值会在某个时间点返回,但有时也会不返回。这就是为什么 promise 同时允许您处理发生的错误。

promise 通过调用函数并将其存储在一个结构中来工作。仅此一项并没有多大作用,但 promise 的关键是您可以无限地链接它们。当您创建了第一个之后,您可以链接下一个 promise 调用,后者将从前者中获取输入。因此,如果您的第一个 promise 返回一个 String,比如您可以在第二个 promise 调用中使用这个返回值 —— 把它转换成 Int,接下来在第三个调用中获取到 Int 并执行其他操作。

promise 依赖两个函数调用:thencatchthen 接收当前 promise 的值,并允许您将其映射到其他东西或仅使用它。catch 是后备函数,它可以捕获沿途发生的任何错误并允许您对其采取措施。但是,必须至少有一条 catch 语句。一个标准的 promise 链如下:

database
    .findOne({ email: request.email })
    .then(user => {
    	if (!user) {
        	// the user doesn't exist, so let's create it
        	return service.registerUser(request.data)
    	} else {
        	return null
    	}
	})
    .then(registerStatus => {
    	// do something after registration
	})
    .catch(error => {
    	// handle error
	})

上面代码主要是如果数据库中用户已经不存在,则尝试去注册一个新的用户。如果它确实已经存在,您将返回 null 或 undefined,然后进一步处理它。如果发生了错误,您将在 promise 链的 catch 语句中捕获到错误。

promise 看起来非常简洁明了,且易于使用和学习,但也有一些注意事项。您不能从 promise 中返回值,您只能返回一个 promise,因为值可能不存在。并且您只能依赖调用链结构。这将使您完全依赖 promise,这对不依赖多线程的 Web 应用来说非常有用。

但是,对于像依赖多线程的 Android 应用来说,就不是很友好了。在现代的应用中您通常想在后台线程执行一些操作,然后需要在主线程消费它们返回的值,因为 promise 运行在单线程,所以它无法实现这种行为。

此外,如果您有多个函数流,则必须在原始 promise 中处理它们。由于您在最外层的 promise 中将响应返回给用户,因此您必须将所有情况都传递到内部 promise 的某个地方。这往往是很笨重的,并且也需要很多实用程序类,就只是为了掩埋多余的代码。promise 最糟糕的部分是,如果您忘记从其中一个链式调用中返回值,整个链的后续部分将无法继续执行,因为它没有任何值可消费。

future 模式与 promise 基于相同的原则,但是它有不同的行为表现。让我们看看它俩有什么不同之处吧。

Thinking About the Future

Future 听起来有点像 promise,同时它们的表现也很相似。当您创建 future 时,您不能保证某个值会在沿线的某个地方出现,因为承诺很容易被打破。您明确表示该值将存在,或者您不得不面对一些其他后果。在 promise 中,这很容易被打破:其中一个链式调用没有返回值将导致整个链没有任何东西可消费,进而整个函数调用都会被冻结。

在 future 中,您必须为函数定义返回语句,否则您将得到编译期错误。此处,一旦您创建了 future,您可以使用 isDone 在任何时间点检查它的状态。如果 isDone 返回 true,您就可以使用 future 返回的值了。

当您准备好使用 future 的返回值时,您可以调用 get 方法去获取它。分析 future 的内部实现将会很有趣。future 使用 executor 来执行它的任务。它们使用线程池处理每个 future 任务的线程和执行,并且您创建的 future 可以并行执行。您将在 第7章:上下文切换和调度 中了解更多关于 executorscheduling 和线程池的信息。

因为基于 Java 的 API 没有挂起的概念,您每次调用 future 时都将是阻塞的。比如:调用 get 方法可能会立马阻塞主线程很长时间。让我们看看 future 是如何运作的以及它们的样子:

private static ExecutorService executor =  Executors.newSingleThreadExecutor();

public static Future<Integer> parse(String input) {
    return executor.submit(() -> {
		Thread.sleep(1000)
		return Integer.parseInt(input);
    });
}

在上面的代码片段中创建了一个 executor,它使用新的线程去执行它的业务。如果您调用 parse 方法并传入一个 String,它将返回一个 future 对象,同时它内部将等待一秒钟然后返回 StringInteger 值。调用代码可能如下所示:

public static void main(String... args) {
    Future<Integer> parser = parse("310");
    while(!parser.isDone()) {
        // waiting to parse
    }
    
    int parsedValue = parser.get();
}

在上面的代码中,您创建了一个 future 对象,同时它将在 executor 创建的线程中开始执行它的任务。一旦您调用 isDone 返回 true,您就知道它可以使用了。最后,通过调用 get 方法,您就可以从 future 中得到值了,该值缓存在 future 对象中并且可以重复使用。

futurepromise 更加灵活,但是它同样也有缺点:必须等待值,无论是 get 方法的阻塞还是自旋直到 future 完成,这都是阻塞的。

future 非常适合在不同的线程处理和返回值。当您想并行执行时,您可以为 future 创建多个线程,然后在同一时间使用并执行任务。

future 也有简洁明了的控制流程逻辑,因为可以使用 顺序的代码 返回值,且不依赖回调或链式函数调用。但是,另一方面,它总是以阻塞的方式返回值,因此,当您有要呈现的用户界面时,等待的成本可能会很高。

让我们看看上述两种方法的主要区别以及哪个方法与 async/await 更相似。

方法的差异化

将 promise 与 async/await 区分开来的关键特征是 promise 依赖于函数调用链,有点像 builder 模式,但最终 promise 是一系列回调。使用 promise 与响应式扩展非常相似,后者在流上对值进行操作。例如,您可以链接转换运算符或延迟正在处理的数据。

使用 promise 的代码初看起来很有条理,但是如果您有多个流程或逻辑路径,您将会得到嵌套的 promise。正因如此,使用 promise 可能是乏味和丑陋的。较新版本的 Javascript 允许您使用 async 和 await 关键字,它们在较低级别上代替 promise 工作,但对您隐藏了样板代码。

然而,future 和 async/await 依赖于使用单一值,将其隐藏在各种设计模式和构造之下。它也允许您将它们的结果作为值,以便您可以编写顺序的代码,而不使用回调,但是必须要暂停代码来等待它们的值。当您需要没有嵌套函数或回调的简洁且易于理解的代码时,它们都是很棒的机制。

但是,对于急切地等待值,如果您没有处理好线程,可能会导致冻结 UI,甚至可能会造成死锁,因为您的函数在等待结果时可能根本不会返回值。future 和 async/await 的区别在于,future 依赖于阻塞调用,这肯定会冻结您当前所在的线程。

反过来,async/await 模式依赖于挂起函数,比如 Kotlin 协程 API。因此,它减少了阻塞代码,但它也有一些警告。

让我们看看在协程 API 中是如何实现 async 和 await 的,如何利用它们来发挥您的优势,以及存在哪些机制来阻止您破坏您的代码或程序,以防数据不显示。