Asynchronous programming(异步编程)
Dart 的异步编程用于表达“结果稍后才会到达”的代码。它不会让同步计算自动并行执行,而是让当前 isolate 在等待 I/O、计时器或其他异步结果时,把执行权交还给事件循环。
Dart 中最常用的异步工具是:
Future<T>:一次异步结果;Stream<T>:多次异步结果;async和await:用接近同步代码的写法等待异步结果;async*、yield和await for:创建并消费异步数据流。
Future 表示一次异步结果
Future<T> 表示一个将来完成的结果。它最终只会完成一次:要么得到一个 T 类型的值,要么以错误结束。
Future<String> fetchUserName() async {
await Future<void>.delayed(const Duration(milliseconds: 100));
return 'Ada';
}
Future<void> main() async {
final name = await fetchUserName();
print('Hello, $name');
}
fetchUserName() 被标记为 async,所以它的返回类型是 Future<String>。调用方使用 await 等待这个 Future 完成后,才会得到真正的字符串值。
await 暂停的是当前 async 函数,不是整个程序。等待期间,当前 isolate 的事件循环仍然可以处理其他已经排队的事件。
async 函数先同步执行到第一个 await
async 允许函数内部使用 await,但函数体不会从第一行开始就自动放到后台执行。调用 async 函数后,它会先同步运行,直到遇到第一个 await 或函数结束。
Future<void> printOrder() async {
print('A');
await Future<void>.delayed(Duration.zero);
print('C');
}
Future<void> main() async {
final future = printOrder();
print('B');
await future;
}
输出顺序是:
A
B
C
原因是 printOrder() 先同步打印 A。遇到 await 后,printOrder() 暂停并返回一个 Future<void>,main() 继续打印 B。等待的 Future 完成后,printOrder() 才恢复执行并打印 C。
错误处理要包住 await
异步错误通常要在 try 块中 await 对应的 Future,这样 catch 才能捕获异步完成时产生的错误。
Future<int> parseScore(String input) async {
await Future<void>.delayed(const Duration(milliseconds: 10));
return int.parse(input);
}
Future<void> main() async {
try {
final score = await parseScore('not-a-number');
print(score);
} on FormatException {
print('Invalid score');
}
}
如果只创建 Future 而不在 try 块中等待它,错误可能会在 try-catch 已经结束之后才发生。
Future<void> main() async {
try {
final scoreFuture = parseScore('not-a-number');
print('Future created');
final score = await scoreFuture;
print(score);
} on FormatException {
print('Invalid score');
}
}
这段代码仍然可以捕获错误,因为 await scoreFuture 在 try 块内。重点不是 Future 在哪里创建,而是错误完成时是否被当前错误处理流程等待或注册处理。
并发等待多个 Future
如果多个异步任务互不依赖,可以先启动它们,再一起等待。这样总耗时通常接近最慢的那个任务,而不是所有任务时间相加。
Future<String> fetchProfile() async {
await Future<void>.delayed(const Duration(milliseconds: 200));
return 'profile';
}
Future<String> fetchSettings() async {
await Future<void>.delayed(const Duration(milliseconds: 200));
return 'settings';
}
Future<void> main() async {
final profileFuture = fetchProfile();
final settingsFuture = fetchSettings();
final results = await Future.wait([profileFuture, settingsFuture]);
print(results);
}
这里两个函数在遇到各自的 await 后都把等待交给事件循环。Future.wait() 等待两个 Future 都完成,并返回按输入顺序排列的结果列表。
如果第二个任务依赖第一个任务的结果,就应该顺序 await,不要为了并发而隐藏真实依赖。
Stream 表示多次异步结果
Stream<T> 表示一串随时间到达的异步值。它适合文件分块读取、用户输入、WebSocket 消息、周期性计时器等场景。
Stream<int> countTo(int max) async* {
for (var i = 1; i <= max; i++) {
await Future<void>.delayed(const Duration(milliseconds: 100));
yield i;
}
}
Future<void> main() async {
await for (final value in countTo(3)) {
print(value);
}
}
async* 声明一个返回 Stream 的异步生成器函数。yield 会向流中发出一个值,但不会结束整个函数;循环继续运行时还可以继续发出后续值。
await for 会依次等待流中的每个值。流关闭后,循环结束;循环体抛出异常或使用 break 提前退出时,订阅会被取消。
处理 Stream 错误
消费 Stream 时,可以把 await for 放在 try-catch 中处理错误事件。
Stream<int> numbers() async* {
yield 1;
throw StateError('broken stream');
}
Future<void> main() async {
try {
await for (final value in numbers()) {
print(value);
}
} on StateError catch (error) {
print(error.message);
}
}
Stream 的错误也是流事件的一部分。使用 await for 时,错误会表现为循环处抛出的异常,因此可以用普通的 try-catch 处理。
异步不等于并行
async/await 让异步流程更容易读写,但不会把 CPU 密集型同步计算变成并行计算。同一个 isolate 中的 Dart 同步代码一次只执行一段。
void blockCurrentIsolate() {
final started = DateTime.now();
while (DateTime.now().difference(started).inSeconds < 2) {
// 模拟长时间同步计算。
}
print('finished');
}
这类代码会占住当前 isolate。即使函数外层包了 async,同步循环本身仍然会阻塞事件循环。需要利用多核并行执行 CPU 密集型任务时,应考虑 Isolate。
TypeScript 对比:TypeScript 中的
Promise和async/await可以类比 Dart 的Future与await,都用于等待一次异步结果。关键差异是 Dart 的并行计算通常使用 isolate,而不是把Future当作线程。
需要记住的差异
Future<T>表示一次异步结果,Stream<T>表示多次异步结果。async函数会先同步执行到第一个await。await暂停当前async函数,等待期间事件循环可以处理其他事件。- 异步等待适合 I/O 和定时器;CPU 密集型同步计算仍然会阻塞当前 isolate。
- 处理异步错误时,要确保对应的
Future或Stream被当前错误处理逻辑等待或订阅。