Dart 中的 async 和 await

async 和 await 的语法在很多语言中都有,比如 js, python, rust, swift。dart 中也有。我们来讨论一下这种语言中 async / await 的使用方法和背后的运行逻辑。

我们按照 async 和 await 的“生命周期”来逐步讨论这种语法的使用,也就是说按照这样的顺序:

  1. 声明
  2. 调用
  3. 运行

声明

async 可以用来声明一个异步函数。比如

1
2
3
Future<String> request() async {
return "test";
}

这个函数会立即返回一个 Future。Future 会在执行完成时,可以取到内部的值。至于 Future 何时执行完成,在「运行」部分进行讨论。

这里为了举例方便,用了一个非常简单的 Future。

但 async 某种程度上类似于一种语法糖 1 2 ,利用 Future 类的 API(Future 是一个抽象类)构造一个以 Future 为返回值的函数 3 ,效果是一样的。比如

1
2
3
Future<String> request() {
return Future<String>.value("test");
}

调用

要想取到 Future 的完成之后的值,可以用 await 等待 Future 完成,比如 await request() 。但是要想使用 await,必须在另外一个 async 函数的函数体内(注,用 await 必须用 async,但是用 async 不一定要在其中用 await)。

1
2
3
4
Future<String> handleResponse() async {
final resp = await request();
return "got response: $resp";
}

同样,await 也类似于一种语法糖,相当于把 await 右边的语句当成一个 Future,然后把 await 之后代码当成是 Future 完成之后的回调。我们可以用 Future 的 API 实现相同的效果,比如

1
2
3
Future<String> handleResponse() {
return request().then((resp) => "got response: $resp");
}

then 方法的返回值依然是一个 Future,可以继续串联 then,而传入的 then 的参数始终是上一个 Future 完成之后的值。但如果需要多个 then 串联,可能就还是写 await 更方便。

但这里有一个问题,async 函数和 Future.then 方法返回的还是一个 Future,要想得到这个 Future 的值就需要在另外一个 async 函数中调用,或者继续串联 then ,什么时候才能停止这个 Future 的链条呢?

首先,想要在 Future 的链条外得到 Future 的执行后的结果,或者说从异步的执行中,回到同步执行的代码中,是不可能的 4 5

比如下面这段代码,即使等待足够长的时间,Future 也无法完成对变量 a 的赋值,然后继续执行 main 中剩下的同步代码,所以最后一行代码打印出的值还是 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';

void main() {
int a = 0;
Future.delayed(Duration(seconds: 1), () {
a = 1;
print("future: $a");
});
sleep(Duration(seconds: 2));
print("last line: $a");
}
// 输出结果:
// last line: 0
// future: 1

其次,也没必要这样做,实际生产的 Dart 项目,比如一个 Flutter app,本来就是不断产生事件,处理事件的过程,其中不断产生 Future,执行 Future,Future 用过之后自然抛弃(只要没有被继续引用,就会自动被垃圾回收),没有必要回到 main 的同步程序中,也回不去。

这些都跟 Dart 的「运行」方式有关。

运行

Future 在 Dart 中的运行方式大致是这样的:

  1. Dart app 启动
  2. 产生一个 Isolate(main isolate) 6
  3. 初始化 2 个事件队列 6
  4. 在 main isolate 中执行 main() 函数7 8
  5. main() 中产生的 Future 会被注册到事件队列中(刚刚两个事件队列中的一个)6
  6. main() 完成并退出
  7. main() 退出后,Event Loop 启动(每个 Isolate 有自己唯一且独立的 Event Loop),按顺序处理队列中的事件和 Future 7 8
  8. Event Loop 处理事件队列的过程中,如果遇到新的事件或者产生新的 Future,会继续插入到事件队列中
  9. 处理完事件之后,以及没有新的事件插入到事件队列中,main isolate 会退出 7 8
  10. Dart app 运行结束

其中,有一些细节会影响到 Future 执行的顺序。

首先,main() 需要退出才能进入 Event Loop,Future(或事件)才能被处理。在 main() 执行的阶段,只有同步执行的代码会被真正的运行,而 Future 中的方法不会被执行。

所以,我们会看到,如果阻塞住 main 中的同步过程,Future 的时效性无法保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';

void main() {
print("1st line: ${DateTime.now()}");
Future.delayed(
Duration(seconds: 1), () => print('delay finished: ${DateTime.now()}'));
sleep(const Duration(seconds: 2));
print("last line: ${DateTime.now()}");
}

/* 输出结果:
1st line: 2022-11-22 17:08:04.968417
last line: 2022-11-22 17:08:06.975185
delay finished: 2022-11-22 17:08:06.978442
*/

其中,Future 不是延迟 1 秒执行,而是等待了 2 秒之后,在 main 退出之后立即执行的。

其次,Isolate 有且只有一个线程,或者说每个 Isolate 只有一个 Event Loop,一旦被占用,这个 Event Loop 就没法处理其他事件,也没法运行那些事件的 handler。而且,Dart 一旦运行一个方法,就会一直把它运行完,中途不会被其他 Dart 代码打断,没有 interleaving 8

这就会导致,即使程序已经进入 Event Loop,虽然可以处理各种异步 Future,但如果某个 Future 的实际运行占用了 Isolate,其他 Future 也无法得到执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'dart:io';

void main() {
Future.delayed(Duration(seconds: 0), () {
print("before sleep ${DateTime.now()}");
sleep(Duration(seconds: 5));
print("after sleep ${DateTime.now()}");
});
Future.delayed(
Duration(seconds: 1), () => print('delayed #1 ${DateTime.now()}'));
Future.delayed(
Duration(seconds: 2), () => print('delayed #2 ${DateTime.now()}'));
print("last line");
}

/* 输出结果:
last line
before sleep 2022-11-22 21:59:42.428351
after sleep 2022-11-22 21:59:47.432718
delayed #1 2022-11-22 21:59:47.437846
delayed #2 2022-11-22 21:59:47.438072
*/

其中,由于第一个进入事件队列的 Future 中有 sleep 5 秒,占用了 Event Loop,后面的 Future 虽然在代码上分别要求延迟 1 秒和 2 秒,但实际上都是在第一个 Future 结束之后,立即执行的。

这和多线程的异步执行程序是不一样的,后者的事件(比如这里的计时结束事件 timer expiration)可以在别的线程进行处理,而不用等待现在的线程处理完上一个方法。

谈到这里,我们可以讨论一下 Dart 这样的单线程设计(一个 Isolate 中只有一个线程,且有独立的内存 memory heap)的优劣。

按照官方文档的说法,优点是不用考虑锁(mutex)和竞争关系(race conditions)7

劣势是如果有事件的 handler 或者 Future 的执行占用 main isolate 的 Event Loop 很长事件,那么 UI 就会卡顿,因为用户点击之类的事件也没有线程可以响应。

所以,开发者有必要把比较耗时的步骤从 main isolate 中挪走。比如,新建一个 Isolate(新的 Isolate 中会有新的线程)来执行那些代码,避免阻塞 main isolate。生成新 Isolate 的方法有 Isolate.spawn() 和 Flutter 中的 compute()7 9

最后,我们上面说一个 Isolate 中有 2 个事件队列,他们被分别称为 microtask 和 event。这两个队列的优先级是不一样的,microtask 更高。 8 6

这也会影响 Future 的执行顺序。比如 Future.then 所产生的 Future 就会插入到 microtask 中 10 ,所以总是会比某些 Future 先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'dart:io';

void main() {
Future(() => print('future #1 ${DateTime.now()}')).then((_) {
sleep(Duration(seconds: 1));
print("slept #1: ${DateTime.now()}");
});
Future((() => print('future #2 ${DateTime.now()}')));
print("last line");
}

/* 输出结果:
last line
future #1 2022-11-23 10:57:26.569081
slept #1: 2022-11-23 10:57:27.573343
future #2 2022-11-23 10:57:27.574044
*/

其中,Future #1 和 #2 是在 main 执行的过程中,就被插入到 event 队列中的。Future #1 执行完成之后回调 then。then 会把传入的方法包装在一个新的 Future 中,插入队列。如果他插入的队列是 event,则程序应该先打印 “future #2”。但实际情况是,then 产生的 Future 被插入到了优先级更高的 microtask 队列中,所以比 Future #2 先执行。

这会带来一个问题,如果 microtask 中总是有待执行的 Future,那么 event 中的事件就无法被响应,UI 可能会出现卡顿。这时候可以把 Future #1 改成:

1
2
3
4
5
6
Future(() => print('future #1 ${DateTime.now()}')).then((_) {
Future(() {
sleep(Duration(seconds: 1));
print("slept #1: ${DateTime.now()}");
});
});

这样,在 then 中重新生成一个 Future,那么耗时的步骤会被插入 event 队列的末尾,和其他 Future 一起排队,这样可以给其他 Future 一些执行的机会 8

上面我们都是直接讨论 Future 的执行顺序,把这些 Future 改写成 async 和 await 也是一样的。

在 async 函数中,Dart 会把

  1. await 之前的代码同步执行
  2. await 右侧的表达式也会被同步执行,该表达式的结果会被注册为一个 Future(如果结果本身就是 Future,则直接使用;否则,利用 Future.value() 创造一个 Future)11 ,插入到相应的队列中(后一种情况插入的是 microtask 12
  3. await 之后的代码当作 Future 完成后的回调 6
    举例来说是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void main() {
whatHappensAroundAwait();
print("last line of main()"); // 优先于 await 之后的代码进行打印
}

void whatHappensAroundAwait() async {
print("before await");
final result = await () {
print("right to the await"); // 同步执行
return "result";
}();
print(result); // 在 Future 的回调中执行
print("after await");
}

/* 输出结果:
before await
right to the await
last line of main()
result
after await
*/

我们也可以说,如果一个 async 函数中没有 await,则他本身跟同步函数的执行顺序没有什么区别,只不过返回值会被包装在一个 Future 里(然后注册到 microtask 里)。

对于 await 如何阻塞线程,这取决于 await 右侧的表达式。如果该表达式在同步执行阶段,或者产生的 Future 在执行阶段比较耗时,都会长时间阻塞线程。

如果直接把 main 定义为 async,那么 main 会执行到第一个 await(包含该 await 右边的表达式),把 await 后面的语句当作 future 和该 future 完成之后的回调, 然后 main 就可以退出,进入 Event Loop 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void main() async {
print("1st line of main()");
Future.microtask(
() => print("future #1: cannot be executed without main() exit"));

print("before await"); // 先于 future #1 进行打印
final result = await () {
print("right to the await"); // 先于 future #1 进行打印
return "result";
}();
print(result);
print("after await");
}

/* 输出结果:
1st line of main()
before await
right to the await
future #1: cannot be executed without main() exit
result
after await
*/

其中,await 前面和右面的代码都是在 main 中同步执行的,如果有打印,则先于 future #1 进行打印。而 await 之后的代码,需要 await 产生的 Future 回调时,才能执行,插入到事件队列(microtask)中的时间晚于 future #1,所以,比 future #1 打印晚一些。(BTW,如果 future #1 使用 Future() 构造,而不是用 Future.microtask() ,则会被插入到 event 队列中,执行顺序则会晚于 await 之后的回调,因为 await 右边的 Future 和之后的回调都插入的是 microtask,优先级更高)

1. ES6 入门教程 https://es6.ruanyifeng.com/#docs/async
2. Async and Await: Syntactic Sugar For Promises in Javascript | by Matt McAlister | Medium https://medium.com/@matt.mcalister93/async-and-await-syntactic-sugar-for-promises-in-javascript-aee7ace36d14
3. Language tour | Dart https://dart.dev/guides/language/language-tour#handling-futures
4. dart - Getting values from Future instances - Stack Overflow https://stackoverflow.com/questions/46579358/getting-values-from-future-instances
5. flutter - Return a value from Future in dart - Stack Overflow https://stackoverflow.com/questions/56111615/return-a-value-from-future-in-dart
6. Flutter - Futures - Isolates - Event Loop https://www.didierboelens.com/2019/01/futures-isolates-event-loop/
7. Concurrency in Dart | Dart https://dart.dev/guides/language/concurrency#how-isolates-work
8. The Event Loop and Dart | webdev.dartlang.org https://web.archive.org/web/20170704074724/https://webdev.dartlang.org/articles/performance/event-loop
9. Dart asynchronous programming: Isolates and event loops | by Kathy Walrath | Dart | Medium https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a
10. then method - Future class - dart:async library - Dart API https://api.dart.dev/stable/2.18.5/dart-async/Future/then.html
11. Dart Language Specs https://dart.dev/guides/language/specifications/DartLangSpec-v2.10.pdf
12. sdk/future_impl.dart at 212d5e3a31e82f156ba1e9996ff2cf70d959582d · dart-lang/sdk · GitHub https://github.com/dart-lang/sdk/blob/212d5e3a31e82f156ba1e9996ff2cf70d959582d/sdk/lib/async/future_impl.dart#L639