Isolates(隔离并发单元)
Isolate 是 Dart 中用于并行执行代码的隔离运行单元。每个 isolate 都有自己的内存和事件循环,不能直接访问另一个 isolate 中的对象引用或可变状态;isolate 之间通过消息传递通信。
普通的 Future、Stream 和 async/await 解决的是“等待时不阻塞当前 isolate”的问题。Isolate 解决的是“把耗时计算放到另一个隔离单元中执行”的问题。
使用 Isolate.run() 执行一次计算
一次性的 CPU 密集型任务优先使用 Isolate.run()。它会在另一个 isolate 中执行回调,并把结果作为 Future 返回给当前 isolate。
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 total = await Isolate.run(() => countPrimes(100000));
print(total);
}
这里的质数统计是同步计算。如果直接在主 isolate 中运行,计算期间主 isolate 不能处理其他事件。放入 Isolate.run() 后,当前 isolate 可以等待这个 Future,耗时计算则在另一个 isolate 中完成。
Isolate.run() 适合“传入数据、执行一次、返回结果”的任务,例如解析大型 JSON、压缩数据、计算报表或处理图片像素。
isolate 不共享可变内存
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 修改的是它自己 isolate 中的 counter。主 isolate 中的 counter 不会因此变成 10。实际输出会体现两边状态相互隔离。
这种模型减少了共享可变状态带来的竞态问题。代价是 isolate 之间不能随意共享对象,必须通过消息传递交换数据。
可发送对象和消息传递
isolate 之间通过消息通信。常见可发送消息包括 null、布尔值、数字、字符串、列表、映射、SendPort,以及由这些可发送值组成的数据结构。
import 'dart:isolate';
Future<void> main() async {
final message = await Isolate.run(() {
final values = [1, 2, 3];
final sum = values.reduce((a, b) => a + b);
return {'values': values, 'sum': sum};
});
print(message);
}
传递大量数据时要注意成本。普通可变对象通常需要复制或以受限制的方式传递;数据越大,消息传递成本越明显。不要把 isolate 当作细粒度函数调用机制频繁传入传出大量对象。
使用 Isolate.spawn() 创建长期 worker
如果任务不是一次性计算,而是需要长期存在并多次收发消息,可以使用 Isolate.spawn()。它通常配合 ReceivePort 和 SendPort 建立通信通道。
import 'dart:isolate';
void worker(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
mainSendPort.send(workerReceivePort.sendPort);
workerReceivePort.listen((message) {
if (message == 'close') {
workerReceivePort.close();
return;
}
if (message case [int value, SendPort replyPort]) {
replyPort.send(value * 2);
}
});
}
Future<void> main() async {
final mainReceivePort = ReceivePort();
await Isolate.spawn(worker, mainReceivePort.sendPort);
final workerSendPort = await mainReceivePort.first as SendPort;
final responsePort = ReceivePort();
workerSendPort.send([21, responsePort.sendPort]);
final response = await responsePort.first as int;
print(response);
workerSendPort.send('close');
responsePort.close();
mainReceivePort.close();
}
这个例子中有两层通信:
- 主 isolate 创建
ReceivePort,把它的SendPort交给 worker; - worker 创建自己的
ReceivePort,再把 worker 的SendPort发回主 isolate; - 主 isolate 拿到 worker 的
SendPort后,就可以向 worker 发送后续消息; - 每次请求可以带上一个 reply port,让 worker 把本次结果发回指定接收端。
Isolate.spawn() 更适合长期后台 worker,例如持续处理任务队列、维护独立状态、反复接收命令并返回结果。
关闭端口和生命周期
ReceivePort 会让 isolate 保持可接收消息的状态。长期 worker 不再需要时,应设计明确的关闭消息,并关闭相关端口。
import 'dart:isolate';
void worker(SendPort mainSendPort) {
final receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
receivePort.listen((message) {
if (message == 'close') {
receivePort.close();
return;
}
mainSendPort.send('received: $message');
});
}
没有关闭策略的 worker 容易长期占用资源。使用 Isolate.run() 时,Dart 会为这次计算管理 isolate 的结束;使用 Isolate.spawn() 时,生命周期通常需要你自己设计。
什么时候不该使用 isolate
不是所有异步任务都需要 isolate。等待网络、文件、数据库或计时器结果时,通常使用 Future、Stream 和 await 就够了。
isolate 更适合以下情况:
- 同步计算会明显阻塞当前 isolate;
- 任务可以用消息输入和输出描述清楚;
- 计算成本高于 isolate 创建和消息传递成本;
- 需要一个长期存在的后台 worker。
如果任务很小,创建 isolate 和传递消息的成本可能比直接执行还高。此时应保持普通异步代码。
TypeScript 对比:TypeScript 没有与 Dart isolate 完全等价的语言机制。在浏览器中可以类比 Web Worker,在 Node.js 中可以类比 Worker Threads;相似点是把工作放到独立执行单元,关键差异是 Dart isolate 是 Dart 运行时的核心并发模型,并且默认通过消息隔离状态。
Isolate.run() 和 Isolate.spawn() 的选择
| 场景 | 优先选择 | 原因 |
|---|---|---|
| 执行一次 CPU 密集型计算 | Isolate.run() | API 简洁,返回 Future,适合一次性结果 |
| 长期后台 worker | Isolate.spawn() | 可以持续接收和发送消息 |
| 普通异步 I/O | Future / Stream | 等待期间不阻塞当前 isolate,通常不需要并行计算 |
| 频繁传递大量可变数据 | 谨慎使用 isolate | 消息传递可能带来明显成本 |
需要记住的差异
Future是异步结果,不代表并行线程;Isolate才是 Dart 中常用的并行执行单元。- 每个 isolate 有独立内存和独立事件循环。
- isolate 之间不能直接共享普通可变对象,要通过
SendPort和ReceivePort传递消息。 - 一次性计算优先用
Isolate.run();长期 worker 才考虑Isolate.spawn()。 - isolate 有创建和通信成本,适合较重的 CPU 任务,不适合替代所有异步代码。