跳到主要内容

Concurrency in Dart(Dart 并发)

Dart 的并发模型由两部分组成:FutureStreamasync/await 让单个 isolate 中的异步任务非阻塞地交错执行;Isolate 则让计算任务运行在独立内存和独立事件循环中,从而使用多个 CPU 核心。

学习 Dart 并发时,重点需要掌握以下问题:

  • 事件循环为什么能让异步代码不阻塞当前 isolate?
  • FutureStream 分别表示什么异步结果?
  • asyncawaitawait 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 中的 Promiseasync/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 中可以用 AsyncIterablefor await...of 类比 Dart 的 Streamawait 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。随后函数暂停并返回一个 Futuremain() 继续打印 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 当作共享内存线程使用。

什么时候选择 FutureStreamIsolate

场景优先选择原因
等待一次 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。