Appearance
Dart Isolate 消息传递:SendPort / ReceivePort / 可发送对象
回到总览:Dart 并发总览:Future / event loop / isolate
建议前置:Dart Isolate、compute() 与无共享多核并发(Isolate.run/compute是"单次任务"入口,本页讲长期 isolate 的双向通信)
一句话定义
Dart 的 isolate 不共享内存,彼此只能通过 SendPort / ReceivePort 传消息。Isolate.run / compute 是"派一个任务、拿一个结果"的一次性通道;而"长期运行的 isolate + 双向通信"需要自己 Isolate.spawn 并管理端口。能传什么对象有严格限制——这是 Dart"隔离并发"的执行基础。
先判断是否该看本页:如果你只是单次 CPU 重活(大 JSON、图片处理、压缩、排序),优先看
11-dart-isolate-compute.md;只有在需要长期后台 worker、多轮消息往返、自己管理端口协议时,再进入本页。
代码索引
对应 Lab:dart-isolate-message-passing
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| Isolate.spawn / SendPort / 可发送对象 | 双向握手、深拷贝证明、TransferableTypedData | isolate_message_passing_demo.dart |
为什么需要
很多工程师会用 Isolate.run / compute 搬 CPU 重活,但遇到"要长期后台 isolate + 双向通信"时就含糊:
Isolate.run和Isolate.spawn有什么区别?- 一句话答:
Isolate.run适合单次短任务,计算完自动退出并返回结果;Isolate.spawn用于拉起长期驻留的双向通信 isolate。(详见下文 §1)
- 一句话答:
- SendPort 和 ReceivePort 是什么关系?为什么首条消息总是 SendPort?
- 一句话答:
ReceivePort是消息接收端(持消息队列),SendPort是消息发送句柄;只有把新建 isolate 的SendPort回传给主 isolate,双方才能建立双向通道。(详见下文 §2)
- 一句话答:
- 哪些对象能跨 isolate 传,哪些不能?
- 一句话答:基础类型、SendPort、TransferableTypedData 可传输;但包含闭包/函数、文件句柄、Native 指针及非 const 的特定对象不能传输。(详见下文 §3)
- 传大对象是"复制"还是"共享"?会不会很贵?
- 一句话答:默认传输是以内存深拷贝(Copy-on-write 或二进制序列化)方式复制,成本较高;大字节数组可使用
TransferableTypedData实现零拷贝(Zero-copy)。(详见下文 §3.2)
- 一句话答:默认传输是以内存深拷贝(Copy-on-write 或二进制序列化)方式复制,成本较高;大字节数组可使用
- Dart 的消息传递和 Go channel、Java 共享内存有什么本质区别?
- 一句话答:Java 基于线程共享堆内存并加锁;Go channel 在共享堆中通信;Dart isolate 则是堆内存彻底物理隔离,纯靠 Port 消息深拷贝。(详见下文 §1)
如果说不清,工程上就很容易出现:
- 把普通对象(含闭包 / 函数)传给 isolate,运行期抛
Invalid argument(s): Illegal argument in isolate message - 以为传对象是"引用共享",改一个另一个也变——其实是深拷贝副本
- 大二进制反复跨 isolate 复制,性能浪费(不知道
TransferableTypedData零拷贝) - 后台 isolate 泄漏:spawn 了没关端口,消息队列一直挂着
底层机制
1. Isolate.run vs Isolate.spawn
Isolate.run(fn)/compute(fn, arg):一次性任务——启动 isolate,跑完返回结果,isolate 结束。适合"搬一次重活"。Isolate.spawn(entry, message):长期 isolate——入口函数常驻运行,通过端口持续双向通信,直到主动关闭。适合"后台常驻处理"(如持续的 WebSocket 转发、重计算服务)。
dart
// 一次性:Isolate.run
final result = await Isolate.run(() => heavyParse(json));
// 长期:Isolate.spawn + 端口
final receivePort = ReceivePort(); // 主 isolate 侧收
await Isolate.spawn(workerEntry, receivePort.sendPort); // 把 sendPort 作为首条消息传过去
// worker 侧拿到 sendPort 后,可以发消息回主 isolate1
2
3
4
5
6
7
2
3
4
5
6
7
2. SendPort / ReceivePort 的关系
对应 Lab:dart-isolate-message-passing
- ReceivePort:接收端(一方持有),
listen处理收到的消息。 - SendPort:发送端(可复制给其它 isolate),
send(msg)投递消息。
关键点:SendPort 可以跨 isolate 传递(它本身是"可发送对象")。所以惯用模式是:主 isolate 建一个 ReceivePort,把它的 sendPort 作为 Isolate.spawn 的首条消息传给 worker——worker 就有了"给主 isolate 发消息"的通道;worker 再回发自己的 SendPort,于是双向通道建立。
dart
void workerEntry(SendPort toMain) {
final fromMain = ReceivePort();
toMain.send(fromMain.sendPort); // 告诉主 isolate:往这个端口给我发
fromMain.listen((msg) {
final result = process(msg); // 处理请求
toMain.send(result); // 结果回主 isolate
});
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
预期现象:端口是一对一的"信箱 + 投递口",消息异步排队到达(不是同步调用,也不阻塞发送方);listen 在收到消息时回调,回调运行在接收方 isolate 自己的线程上。
3. 可发送对象:什么能传、什么不能
跨 isolate 传消息有严格白名单(默认深拷贝):
| 可发送 | 不可发送(抛异常) |
|---|---|
null / bool / num / String | 任意普通类实例(非上述类型) |
List / Map(元素也必须可发送,深拷贝) | 函数 / 闭包(含捕获变量的 lambda) |
SendPort / ReceivePort | 含原生资源对象(File / Socket / Native) |
TransferableTypedData(零拷贝转移所有权) | 共享可变引用(Dart 不支持跨 isolate 共享) |
dart
// 错误:传闭包
await Isolate.spawn(worker, () => compute()); // 运行期抛 Illegal argument
// 正确:传可发送的参数,函数本身必须是入口函数
await Isolate.spawn(worker, jsonString); // String 可传1
2
3
4
2
3
4
预期现象:传 List / Map 是深拷贝——worker 里改它不影响主 isolate 的原始对象(这正是"隔离"的含义:没有共享可变状态)。要传大二进制(图片 / 视频帧)用 TransferableTypedData,它不复制内容,只把所有权从一个 isolate 转给另一个(零拷贝,但转移后原 isolate 不能再访问)。
比喻(两座小岛) :每个 isolate 是一座独立小岛,有自己的土地(堆)。岛与岛之间只能用小船(SendPort 通道)运包裹,包裹是重新打包的副本(深拷贝),不是把岛上的地搬过去。
TransferableTypedData是"冷藏集装箱"——货物不上岸复制,整箱移交(零拷贝),但移交后原岛就没这箱货了。想"共享一块地"?Dart 不允许,这正是它和 JVM 共享堆的根本区别。
4. 与 Java 共享内存、Go channel 的本质对照
| 维度 | Dart isolate | Java 线程 | Go goroutine + channel |
|---|---|---|---|
| 内存 | 独立堆,消息深拷贝 | 共享堆 | 共享堆(channel 传的是引用) |
| 通信 | SendPort 传副本 | 锁 / 原子 / volatile | channel 传引用 + 同步 |
| 竞态 | 天然无共享可变状态 | 靠锁防护 | channel 内同步,共享数据仍要小心 |
| 传递大对象 | 深拷贝(或 TransferableTypedData) | 无需拷贝(共享引用,但要锁) | 无需拷贝(共享引用) |
对照点:Java / Go 是"共享内存 + 同步原语",Dart 是"内存隔离 + 消息副本"。所以 Dart 里没有"数据竞争"问题(代价是拷贝开销),Go 里 channel 虽传引用但用同步语义约束(代价是共享状态仍需小心)。这也解释了跨语言对照页里"Dart 不靠锁做主模型"的说法。
Android / Flutter / Web / Backend 对照
| 维度 | Dart | Kotlin | JS |
|---|---|---|---|
| 并行单元 | isolate(独立堆) | 线程 / 协程(共享堆) | Worker(独立上下文) |
| 通信 | SendPort 深拷贝 | 共享内存 + 锁 | postMessage(结构化克隆) |
| 大对象传递 | TransferableTypedData 零拷贝 | 共享引用(要同步) | Transferable(ArrayBuffer 转移) |
| 竞态防护 | 无共享状态,天然安全 | JMM + 锁 | 无共享状态(postMessage 克隆) |
常见场景
1. 长期后台 isolate:WebSocket / 事件流转发
Isolate.spawn 一个常驻 isolate 接收外部数据,通过 SendPort 把解析结果持续发回主 isolate,主 isolate 只做 UI 更新。
2. 重计算服务化
把加解密 / 编解码 / 复杂清洗做成"常驻 isolate + 请求-响应"模式,避免每次任务都重启 isolate(Isolate.run 每次都有启动开销)。
3. 大图片 / 视频帧零拷贝
TransferableTypedData 把解码后的字节整箱移交给另一个 isolate 处理,避免深拷贝的大内存消耗。
常见误配、事故后果与排障
| 坑 | 现象 | 修法 |
|---|---|---|
| 传闭包 / 普通对象 | 运行期 Illegal argument in isolate message | 只传可发送类型;逻辑放入口函数 |
| 以为传对象是共享引用 | worker 改了、主 isolate 没变,逻辑对不上 | 明确"深拷贝"语义,需要回传结果再 send 回来 |
| 大 List 反复跨 isolate | 内存翻倍、卡顿 | 用 TransferableTypedData 或减少传递频率 |
| spawn 后不关端口 | isolate / 消息队列泄漏 | 结束时 receivePort.close(),或按需 kill |
| 忘了首条消息传 SendPort | worker 无法回信 | 惯用模式:spawn 时把主侧 sendPort 作为首条消息 |
与相近概念对比
| 概念 | 本质区别 |
|---|---|
Isolate.run / compute | 一次性任务通道;Isolate.spawn 是长期 + 双向 |
Future | 单值异步(可跨 isolate 拿结果);端口是持续通信 |
| Go channel | Go 传引用 + 同步;Dart 传深拷贝副本 |
| Java 共享内存 | 共享堆靠锁;Dart 无共享可变状态 |
JS postMessage | 结构化克隆(语义类似 Dart 深拷贝);Dart 有 TransferableTypedData 对应 JS Transferable |
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| dart-isolate-message-passing | 双向握手、深拷贝证明(子 isolate 改副本主 isolate 不变)、TransferableTypedData 零拷贝 | isolate_message_passing_demo.dart |
复习检查题
Isolate.run和Isolate.spawn的区别?答:
Isolate.run是一次性任务——跑完返回结果、isolate 结束;Isolate.spawn启动长期常驻 isolate,配合 SendPort/ReceivePort 持续双向通信,直到主动关闭。为什么首条消息通常传 SendPort?
答:
SendPort本身可跨 isolate 传递,把主 isolate 的receivePort.sendPort作为首条消息传给 worker,worker 就获得了"给主 isolate 发消息"的通道;worker 再回传自己的 sendPort,双向通信建立。哪些对象不能跨 isolate 传?
答:任意普通类实例、函数 / 闭包、含原生资源的对象(File / Socket / Native)不能传;只能传基本类型、String、元素可发送的 List/Map、SendPort/ReceivePort、TransferableTypedData。
传 List/Map 是共享还是复制?
答:深拷贝副本。worker 里修改不影响主 isolate 的原始对象——这正是 Dart 无共享可变状态、天然无数据竞争的基础;代价是大对象拷贝开销,可用 TransferableTypedData 零拷贝优化。
为什么 Dart 不需要锁?
答:isolate 之间不共享可变内存(消息是深拷贝副本),不存在"多个执行单元同时改同一份数据"的竞态前提;共享状态需求必须显式通过消息来回传,而不是靠锁同步。
速记
- isolate 不共享内存,只传消息(深拷贝副本)
Isolate.run一次性;Isolate.spawn长期 + 双向- ReceivePort 收、SendPort 发,首条消息传 sendPort 建立双向
- 可传:基本类型 / String / List·Map(深拷贝)/ 端口 / TransferableTypedData
- 不可传:普通对象 / 函数 / 原生资源
- 大二进制用 TransferableTypedData 零拷贝
- 记得 close 端口防泄漏