跳到主要内容

Asynchronous programming(异步编程)

Dart 的异步编程用于表达“结果稍后才会到达”的代码。它不会让同步计算自动并行执行,而是让当前 isolate 在等待 I/O、计时器或其他异步结果时,把执行权交还给事件循环。

Dart 中最常用的异步工具是:

  • Future<T>:一次异步结果;
  • Stream<T>:多次异步结果;
  • asyncawait:用接近同步代码的写法等待异步结果;
  • async*yieldawait 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 scoreFuturetry 块内。重点不是 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 中的 Promiseasync/await 可以类比 Dart 的 Futureawait,都用于等待一次异步结果。关键差异是 Dart 的并行计算通常使用 isolate,而不是把 Future 当作线程。

需要记住的差异

  • Future<T> 表示一次异步结果,Stream<T> 表示多次异步结果。
  • async 函数会先同步执行到第一个 await
  • await 暂停当前 async 函数,等待期间事件循环可以处理其他事件。
  • 异步等待适合 I/O 和定时器;CPU 密集型同步计算仍然会阻塞当前 isolate。
  • 处理异步错误时,要确保对应的 FutureStream 被当前错误处理逻辑等待或订阅。