跳到主要内容

Isolates(隔离并发单元)

Isolate 是 Dart 中用于并行执行代码的隔离运行单元。每个 isolate 都有自己的内存和事件循环,不能直接访问另一个 isolate 中的对象引用或可变状态;isolate 之间通过消息传递通信。

普通的 FutureStreamasync/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()。它通常配合 ReceivePortSendPort 建立通信通道。

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。等待网络、文件、数据库或计时器结果时,通常使用 FutureStreamawait 就够了。

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,适合一次性结果
长期后台 workerIsolate.spawn()可以持续接收和发送消息
普通异步 I/OFuture / Stream等待期间不阻塞当前 isolate,通常不需要并行计算
频繁传递大量可变数据谨慎使用 isolate消息传递可能带来明显成本

需要记住的差异

  • Future 是异步结果,不代表并行线程;Isolate 才是 Dart 中常用的并行执行单元。
  • 每个 isolate 有独立内存和独立事件循环。
  • isolate 之间不能直接共享普通可变对象,要通过 SendPortReceivePort 传递消息。
  • 一次性计算优先用 Isolate.run();长期 worker 才考虑 Isolate.spawn()
  • isolate 有创建和通信成本,适合较重的 CPU 任务,不适合替代所有异步代码。