Concurrency in Dart(Dart 并发)
Dart 的并发模型由两部分组成:Future、Stream 和 async/await 让单个 isolate 中的异步任务非阻塞地交错执行;Isolate 则让计算任务运行在独立内存和独立事件循环中,从而使用多个 CPU 核心。
学习 Dart 并发时,重点需要掌握以下问题:
- 事件循环为什么能让异步代码不阻塞当前 isolate?
Future和Stream分别表示什么异步结果?async、await和await for如何改变代码的执行顺序?- 异步执行和并行执行有什么区别?
- 什么时候需要使用
Isolate?
先区分异步和并行
异步不等于并行。异步代码可以在等待 I/O、计时器或其他外部结果时把执行权交还给事件循环,让当前 isolate 继续处理其他事件;但同一个 isolate 内的 Dart 代码仍然一次只执行一段同步代码。
Future<String> loadUserName() async {
await Future<void>.delayed(const Duration(milliseconds: 100));
return 'Ada';
}
Future<void> main() async {
print('start');
final name = await loadUserName();
print('user: $name');
print('end');
}
await 会暂停当前 async 函数,等待 Future 完成后再继续执行后面的语句。暂停期间,当前 isolate 的事件循环可以处理其他已排队的事件。
如果代码本身是长时间运行的同步计算,例如压缩图片、解析大型数据或复杂数学计算,它仍然会占住当前 isolate。此时仅使用 async/await 不能让计算并行执行,需要考虑 Isolate。
TypeScript 对比:TypeScript 中的
Promise和async/await也常用于非阻塞异步流程。两者相似点是await都会等待一个将来完成的结果;关键差异是 Dart 还提供语言和运行时层面的 isolate 模型,用于隔离内存并执行并行计算。
事件循环
每个 Dart isolate 都有自己的事件循环。事件循环从事件队列中取出事件并逐个处理,例如定时器完成、I/O 结果返回、用户输入或回调执行。
可以把它简化理解为:
void eventLoop() {
while (hasNextEvent()) {
processNextEvent();
}
}
bool hasNextEvent() => false;
void processNextEvent() {}
这段代码只是帮助理解模型,不是 Dart 运行时的真实实现。真正需要记住的是:事件处理是按队列推进的,同一个 isolate 中如果有一段同步代码运行太久,后续事件就只能等待。
void blockCurrentIsolate() {
final started = DateTime.now();
while (DateTime.now().difference(started).inSeconds < 2) {
// 模拟长时间同步计算。
}
print('finished');
}
这类同步阻塞会让当前 isolate 暂时无法处理其他事件。命令行程序中表现为后续异步回调变晚;UI 程序中则可能表现为界面卡顿。
Future:一次异步结果
Future<T> 表示一个稍后完成的结果:它最终要么得到一个 T 类型的值,要么以错误结束。
Future<int> fetchScore() async {
await Future<void>.delayed(const Duration(milliseconds: 100));
return 95;
}
Future<void> main() async {
final score = await fetchScore();
print('score: $score');
}
async 函数总是返回 Future。即使函数体中直接 return 95,对调用方来说,它仍然是一个将来完成的 Future<int>。
Future<int> immediateScore() async {
return 95;
}
处理 Future 错误时,要在 try 块中 await 这个 Future,否则异常可能发生在当前 try-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');
}
}
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 提前结束,订阅会被取消。
TypeScript 对比:TypeScript 中可以用
AsyncIterable和for await...of类比 Dart 的Stream与await for。但 Dart 标准库中的Stream还包含订阅、广播流、错误事件等更明确的流式异步 API。
async 函数的执行顺序
async 让函数可以使用 await,但它不会让函数体里的所有代码自动在后台执行。遇到第一个 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() 被调用后先同步执行到第一个 await,打印 A。随后函数暂停并返回一个 Future,main() 继续打印 B。等等待的 Future 完成后,printOrder() 才继续打印 C。
Isolate:隔离内存的并行执行单元
Dart 的普通异步代码运行在当前 isolate 中。需要把耗时计算移出当前 isolate 时,可以使用 Isolate.run() 执行一次独立计算。
import 'dart:isolate';
int countPrimes(int max) {
var count = 0;
for (var candidate = 2; candidate <= max; candidate++) {
var isPrime = true;
for (var factor = 2; factor * factor <= candidate; factor++) {
if (candidate % factor == 0) {
isPrime = false;
break;
}
}
if (isPrime) {
count++;
}
}
return count;
}
Future<void> main() async {
final result = await Isolate.run(() => countPrimes(100000));
print(result);
}
Isolate.run() 会在新的 isolate 中执行回调,并把结果作为 Future 返回给当前 isolate。适合一次性的 CPU 密集型任务。
每个 isolate 都有自己的内存。一个 isolate 中的全局变量、对象引用和可变状态不能被另一个 isolate 直接访问。isolate 之间通过消息传递通信,因此通常不需要锁来保护共享内存。
import 'dart:isolate';
var counter = 0;
Future<void> main() async {
await Isolate.run(() {
counter = 10;
print('worker: $counter');
});
print('main: $counter');
}
这段代码会显示工作 isolate 中修改了它自己的 counter,但主 isolate 中的 counter 仍然是原来的值。不要把 isolate 当作共享内存线程使用。
什么时候选择 Future、Stream 或 Isolate
| 场景 | 优先选择 | 原因 |
|---|---|---|
| 等待一次 I/O 结果 | Future + await | 结果只完成一次,等待期间不阻塞当前 isolate |
| 持续接收多次异步值 | Stream + await for | 值会随时间多次到达 |
| 一次性 CPU 密集型计算 | Isolate.run() | 避免长时间占用当前 isolate |
| 长期后台工作并多次通信 | Isolate.spawn() + ports | 需要长期存在的独立 worker |
如果只是等待网络、文件或数据库结果,通常不需要新建 isolate。异步 I/O 已经可以让当前 isolate 在等待期间继续处理其他事件。
如果同步计算会明显拖慢当前 isolate,再考虑 isolate。isolate 有创建成本和消息传递成本,传递大量数据时也要注意复制或转移数据带来的开销。
需要记住的差异
Future是一次异步结果;Stream是多次异步结果。await暂停的是当前async函数,不是整个程序。async/await让异步流程更容易写,不会自动把同步计算变成并行计算。- 同一个 isolate 内的 Dart 同步代码一次只执行一段;长时间同步任务会阻塞当前 isolate 的事件循环。
Isolate有独立内存和独立事件循环,通过消息通信,适合 CPU 密集型任务或长期后台 worker。