(译)异步编程: Futures
知识点有什么?
- dart代码是执行在单个线程上的
- 阻塞线程的代码能够让你的程序卡死。
-
Future
对象代表的是异步操作的结果-稍后完成的处理或者 I/O 。 - 要实现暂停执行直到一个 future 完成了,在 async 方法中使用
await
(或者使用then()
) - 要捕捉异常,在异步方法中使用
try-catch
表达式(或使用catchError()
) - 要并发执行代码,就创建一个 isolate 。(对于web应用来说,就是子线程)
Dart代码运行在单个线程上。如果dart代码阻塞了,例如,执行一个耗时的计算或者等待I/O,整个程序就会卡死。
异步操作让你的代码完成其他工作的同时等待一个操作完成。dart 使用 Future
对象来表示异步操作的结果。为了使用futures,你可以使用 async ,await ,或者 Future API。
注意:所有Dart代码都在隔离区的上下文中运行,该隔离区拥有Dart代码使用的所有内存。当Dart代码正在执行时,同一个隔离区中的其他代码都无法运行。
如果你希望Dart代码的多个部分同时运行,你可以在单独的隔离区中运行它们。 (Web应用程序使用子线程而不是隔离程序。)多个隔离程序同时运行,通常每个都在其自己的CPU核心上运行。隔离区不共享内存,它们可以进行交互的唯一方法是通过相互发送消息。有关更多信息,请参阅 isolates 或 Web子线程 的文档。
介绍
让我们看下一些可能导致程序卡死的代码:
// 同步代码 void printDailyNewsDigest() { var newsDigest = gatherNewsReports(); // 可能需要一段时间。 print(newsDigest); } main() { printDailyNewsDigest(); printWinningLotteryNumbers(); printWeatherForecast(); printBaseballScore(); }
我们的代码收集了今天的新闻,并将之打印出来,然后打印其他一堆有意思的条目给用户:
Winning lotto numbers: [23, 63, 87, 26, 2] Tomorrow's forecast: 70F, sunny. Baseball score: Red Sox 10, Yankees 0
我们的代码是有问题的:因为 gatherNewsReports()
阻塞了,剩余的代码只有在 gatherNewsReports()
返回文件内容之后才能运行,不管这个过程需要多长时间。如果读取文件花费了很长时间,用户就需要等待,然后开始疑惑如果他们中了乐透,明天的天气会怎么样,以及谁赢了今天的比赛。
为了帮助应用能及时响应,Dart库的作者在定义方法的时候使用了一个异步模型来做潜在的繁重工作。譬如使用 future 返回它们的值的方法。
什么是future
future是一个 Future 对象,用来表示异步操作产生的结果 T。如果结果不是一个可用的值,future的类型就是 Future
。当方法返回的 future 被调用了,有两件事发生了:
- 方法把需要做的工作排队然后返回一个未完成的 Future 对象。
- 稍后,当操作完成了,Future 对象以值或一个错误结束。
当写依赖于future的代码的时候,你有两个选项
- 使用
async
和await
- 使用
Future
API
Async 和 await
async
和 await
关键字是Dart语言 异步支持 的一部分。它们允许你像写同步代码一样写异步代码并且不用使用 Future
API。async 方法的 async
关键字是在它的 body 之前的。 await
关键字只有在异步方法中才能使用。
版本说明:在Dart 1.x中,异步函数立即暂停执行。在Dart 2中,异步函数不是立即挂起,而是同步执行,直到第一个等待或返回。
以下的 app 通过使用 async
和 await
去读取这个网站上的文件内容来模拟读取新闻。
import 'dart:async'; Future printDailyNewsDigest() async { var newsDigest = await gatherNewsReports(); print(newsDigest); } main() { printDailyNewsDigest(); printWinningLotteryNumbers(); printWeatherForecast(); printBaseballScore(); }
Winning lotto numbers: [23, 63, 87, 26, 2] Tomorrow's forecast: 70F, sunny. Baseball score: Red Sox 10, Yankees 0
注意下 printDetailDigest()
是首个被调用的方法,但是新闻却是最后被打印出来的,即使这个文件只包含了单行的信息。这是因为读取和打印文件的的代码是异步运行的。
在此例中, printDetailNewsDigest()
方法调用了gatherNewReports,它是非阻塞的。调用 gatherNewsReports()
把要做的工作放入队列但是不阻止剩余的代码执行。程序打印出了,乐透号码、天气预报、以及棒球的比分; 当gatherNewsReports()
结束收集新闻的时候,程序把它打印出来。如果读取 gatherNewsReports()
花费了一小会的时间来完成它的工作,并不会造成什么伤害,在每天新闻打印之前用户可以看其它的东西。
注意下返回类型, gatherNewsReports()
的返回类型是 Future
,这意味着它是以string值结束的 future 。 printDailyNewsDigest()
方法,不会返回一个值,它的返回类型是 Future
。
以下的示意图展示了代码中的执行流程。每个编码对应一个步骤。
- 应用开始执行
-
main()
方法调用 async 方法printDailyNewsDigest()
,它就开始异步执行。 -
printDailyNewsDigest()
使用await来调用方法gatherNewsReports()
,它就开始执行。 -
gatherNewsReports()
方法返回一个未完成的 future(一个 Future的实例)。
- 因为
printDailyNewsDigest()
是一个异步的方法并且在等待一个值,它暂停了它的执行并返回一个未完成的 future .(在此例中,一个Future
被返回到了它的调用者main()
)。 - 剩余的打印方法执行。因为它们是同步的,每个方法在移动到下一个打印方法之前已经执行完全了。例如,中奖的乐透号码是在天气预报打印之前的。
- 当
main()
方法执行完成,异步方法就能恢复执行了。首先,由gatherNewReports()
返回的 future 完成了。然后printDailyNewsDigest()
继续执行,打印出新闻。 - 当
printDailyNewsDigest()
方法体完成了执行,这个最初返回的future完成了,然后应用就退出了。
注意下,async 方法开始是直接执行的(同步的)。但它遇到下面任何一种情形的时候,方法暂停了执行并返回了一个未完成的 future:
- 方法的首个 await 表达式(在表达式中获取未完成的 future 之后)。
- 方法中任意的 return 声明
- 函数体的结束
错误处理
如果返回类型为 Future 的函数以错误结尾的时候,你可能想要捕捉这个错误。异步函数可以使用 try-catch 处理错误:
Future printDailyNewsDigest() async { try { var newsDigest = await gatherNewsReports(); print(newsDigest); } catch (e) { // Handle error... } }
try-catch 代码与异步代码的行为方式与同步代码的行为方式相同:如果 try
块中的代码抛出异常,则 catch
子句中的代码将执行。
顺序处理
你可以使用多个 await
表达式来确保每个语句在执行下一个语句之前完成:
// 使用 async 和 await 的线性处理. main() async { await expensiveA(); await expensiveB(); doSomethingWith(await expensiveC()); }
在 expensiveA()
完成之前, expensiveB()
函数不会执行,依此类推。
Future API
在 Dart 1.9 中添加 async
和 await
之前,你必须使用 Future API。你可能仍会看到旧代码中使用的 Future
API以及需要比 async-await 提供的功能写更多的代码。
要使用Future API编写异步代码,可以使用 then() 方法注册回调。当 Future 完成时,此回调将触发。
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:async'; Future printDailyNewsDigest() { final future = gatherNewsReports(); return future.then(print); // You don't *have* to return the future here. // But if you don't, callers can't await it. } main() { printDailyNewsDigest(); printWinningLotteryNumbers(); printWeatherForecast(); printBaseballScore(); } printWinningLotteryNumbers() { print('Winning lotto numbers: [23, 63, 87, 26, 2]'); } printWeatherForecast() { print("Tomorrow's forecast: 70F, sunny."); } printBaseballScore() { print('Baseball score: Red Sox 10, Yankees 0'); } const news = ''; const oneSecond = Duration(seconds: 1); // Imagine that this function is more complex and slow. :) Future gatherNewsReports() => Future.delayed(oneSecond, () => news); // Alternatively, you can get news from a server using features // from either dart:io or dart:html. For example: // // import 'dart:html'; // // Future gatherNewsReportsFromServer() => HttpRequest.getString( // 'https://www.dartlang.org/f/dailyNewsDigest.txt', // );
请注意, printDailyNewsDigest()
是第一个被调用的函数,但即使文件只包含一行,新闻也是最后要打印的内容。这是因为读取文件的代码是异步运行的。
应用是按以下步骤执行的:
1.该应用程序开始执行。
2.main函数调用 printDailyNewsDigest()
函数,该函数不会立即返回,而是调用 gatherNewsReports()
。
3. gatherNewsReports()
开始收集新闻并返回Future。
4. printDailyNewsDigest()
使用 then()
指定对 Future
的响应。调用then() 会返回一个新的 Future
,它将使用 then()
的回调返回的值来完成。
5.剩余的打印功能执行。因为它们是同步的,所以在继续下一个打印功能之前,每个功能都会完全执行。例如,在打印天气预报之前打印中奖彩票号码。
6.当所有新闻都到来时, gatherNewsReports()
返回的Future以包含收集的新闻的字符串完成。
7. printDailyNewsDigest()
中 then()
指定的代码运行,打印新闻。
8.该应用程序退出。
注意:在printDailyNewsDigest()函数中,代码future.then(print)等效于以下内 容:future.then((newsDigest)=> print(newsDigest))
或者, then()
内的代码可以使用花括号
Future printDailyNewsDigest() { final future = gatherNewsReports(); return future.then((newsDigest) { print(newsDigest); // Do something else... }); }
你需要为 then() 的回调提供一个参数,即使 Future 的类型为 Future
。按照惯例,未使用的参数名为_(下划线)。
final future = printDailyNewsDigest(); return future.then((_) { // Code that doesn't use the `_` parameter... print('All reports printed.'); });
处理错误
使用 Future
API,你可以使用 catchError()
捕获错误:
Future printDailyNewsDigest() => gatherNewsReports().then(print).catchError(handleError);
如果新闻流不可用于读取,则上述代码执行如下:
-
gatherNewsReports()
返回的 future 以错误结束。 -
then()
返回的未来以错误完成;不调用print()
。 -
catchError()
(handleError()
)的回调处理错误,catchError()
返回的 future 正常完成,错误不会传播。
将 catchError()
链接到 then() 是使用 Future
API时的常见模式。考虑将 Future
API 等同于 try-catch 块。
与 then() 一样,catchError() 返回一个新的 Future,它以其回调的返回值结束。
有关更多详细信息和示例,请阅读 Futures和错误处理 。
调用多个返回futures的函数
考虑下返回Future对象的三个函数, expensiveA()
, expensiveB()
和 expensiveC()
。你可以按顺序调用它们(一个函数在前一个函数完成时启动),或者你可以同时启动所有函数,并在所有值返回后执行某些操作。Future 接口足够流畅,可以处理这两种用例。
使用then()链接函数调用
当 Future-returns 函数需要按顺序运行时,使用链接的 then() 调用:
expensiveA() .then((aValue) => expensiveB()) .then((bValue) => expensiveC()) .then((cValue) => doSomethingWith(cValue));
嵌套回调也有效,但它们更难阅读而不是Dart-y。
使用Future.wait() 等待多个future完成
如果函数的执行顺序不重要,可以使用Future.wait()。
当你传递 Future.wait() 一个Future列表时,它会立即返回一个 Future 。在所有给定的Future 完成之前,future 不会完成。然后它以一个列表结束,该列表包含原始列表中每个future的值。
Future.wait([expensiveA(), expensiveB(), expensiveC()]) .then((List responses) => chooseBestResponse(responses, moreInfo)) .catchError(handleError);
如果任何调用的函数因错误而完成,则 Future.wait()
返回的 Future
也会以错误完成。使用 catchError()
来处理错误。