Isolate:真并行,以及你多半不需要它
Dart 确实有真并行——但它不叫「线程」,叫 isolate,而且它不共享内存,两个 isolate 之间传数据要靠复制。它更像多进程。这一章讲清楚:什么时候你才真的需要它(比你想的少)、怎么用一行 Isolate.run 搞定,以及它为什么让 Dart 的并发从「危险」变成了「无脑安全」。
先分清两种「重活」
第 18 章埋了这个区分,这里说透。你的 App 会遇到两种拖慢它的活,它们的解法完全不同:
| 等待型(I/O-bound) | 计算型(CPU-bound) | |
|---|---|---|
| 特征 | 大部分时间在等(网络、磁盘、数据库) | 大部分时间在算(CPU 满载) |
| 例子 | 拉课程列表、读缓存文件 | 解析超大 JSON、图片处理、加密、复杂计算 |
| 解法 | await——等待时线程空闲,界面照常刷 | Isolate——真的要另一个核去算 |
| 占比 | 你 95% 的异步 | 少数,但一旦有就必须处理 |
await 的本事是「等待期间把线程让出去」(第 18 章)。但纯计算没有「等待」可让——它每一毫秒都在真真切切地占用 CPU。你把一个跑 800ms 的计算写成 async、加上 await,它照样会把这根线程占满 800ms,界面照样冻 800ms。让出线程的前提是有空闲,而满载的计算没有空闲。这时唯一的出路是:把它搬到另一个核上去算——那就是 Isolate。
Isolate:隔离的执行单元,不共享内存
「isolate」这个名字就是「隔离」。每个 isolate 有自己独立的内存堆和自己的事件循环。你的整个 App 默认跑在一个叫 main isolate 的里面(第 18 章那根线程就是它的)。
关键性质,也是它和线程最大的不同:
两个 isolate 看不到对方的变量、对象、状态。它们之间唯一的沟通方式是传消息——而消息在传递时会被复制一份(除了少数可转移的类型)。
所以它更像多进程,不像多线程:没有共享状态,就没有数据竞争、没有锁、没有 synchronized、没有 volatile。Dart 的并发用「隔离 + 复制」换掉了「共享 + 加锁」——你为多线程安全操的那些心,在这里根本不存在。
用起来:Isolate.run 一行搞定
好消息是,现代 Dart 把常见用法简化到了一行。要在别的核上跑一个计算、拿回结果,用 Isolate.run:
// ✗ 卡界面 800ms final result = parseHugeJson(bigString); // 同步跑在 main isolate // ✓ 甩到另一个 isolate,界面全程流畅 final result = await Isolate.run(() => parseHugeJson(bigString));
就这么简单——Isolate.run 开一个临时 isolate、跑你的函数、把结果复制回来、关掉它。你 await 它就行。这覆盖了 90% 的「计算型重活」需求。
Flutter 还提供一个更老的便捷函数 compute,语义类似(顶层函数 + 一个参数):
final result = await compute(parseHugeJson, bigString);
新代码优先 Isolate.run(更灵活,支持闭包)。要长期运行、双向通信的场景(比如一个后台一直在处理任务的 isolate),才需要手动 Isolate.spawn + SendPort/ReceivePort 那套——但那属于少数情况,用到再查。
1. 传数据要复制,大对象很贵。你传进去的参数、传出来的结果都会被复制一遍。如果数据本身就有几十 MB,复制的开销可能抵消甚至超过并行省下的时间。所以 Isolate 适合「输入输出小、但中间计算重」的活(传一个字符串进去、算很久、返回一个数字),不适合「搬运大量数据」。
2. 启动有开销。开一个 isolate 不是免费的。为一个 1ms 的小计算开 isolate,得不偿失。只有当计算真的会卡出可见的掉帧(几十毫秒以上)时,才值得。
那到底什么时候才真需要它
把判断收敛成一句话:当你在 main isolate 上做一件纯计算的事、它长到会掉帧、而且这件事的输入输出数据量不大——这时才用 Isolate。具体场景:
- 解析一个很大的 JSON/CSV(比如一次性导入几千条成绩)。
- 图片处理:压缩、缩放、滤镜、生成缩略图。
- 加解密、哈希一个大文件。
- 复杂的本地计算:路径规划、数据聚合统计、离线搜索建索引。
你的学校 App 大概率能撞上的就一两个——比如「导入班级名单时解析一个大表格」。除此之外,日常的网络请求、数据库读写、界面交互,全都用 await 就够了,一个 isolate 都不用开。这就是标题说「你多半不需要它」的意思。
别猜,测。用 DevTools 的 Performance 页录一段,看那个卡顿的操作在时间线上占了多少 ms。如果某个同步操作超过 16ms(一帧)、而它是纯计算,就考虑 Isolate.run。如果它其实是在等 I/O(时间线上显示为等待而非 CPU 占用),那你要的是 await,不是 isolate。
一个你天天在享受、却不知道的 isolate
顺带说个冷知识:Flutter 引擎自己会在别的线程上做一些活——比如图片的解码、部分光栅化工作。所以你 Image.network 加载一张大图时,解码不会卡你的 main isolate,这是引擎替你并行了。你能控制的 isolate 是 Dart 层的计算,而引擎层的并行是免费赠送的。这也是为什么 Flutter 的图片加载体验通常很顺。
一个完整例子:导入班级名单不卡界面
把这一章落到你会真的写的代码上。「导入班级名单」——用户选一个几千行的 CSV/Excel,你要解析成对象、可能还要做些校验和统计。这正是「输入是个字符串、中间算很久、输出是结构化数据」的典型,教科书级的 isolate 场景:
// 这个函数会在另一个 isolate 里跑,所以它必须是「自包含」的:
// 不碰任何 Widget、不碰 main isolate 的全局变量,只吃参数、吐结果
List<Student> _parseRoster(String csv) {
return csv
.split('\n')
.skip(1) // 跳过表头
.where((line) => line.trim().isNotEmpty)
.map((line) {
final cols = line.split(',');
return Student(id: cols[0], name: cols[1], className: cols[2]);
})
.toList();
}
// UI 侧:一行甩过去,界面全程能转圈、能取消
Future<void> onImport(String csv) async {
setState(() => _importing = true);
final students = await Isolate.run(() => _parseRoster(csv)); // 另一个核在算
if (!mounted) return; // 第 19 章:回来先检查页面还在不
setState(() { _students = students; _importing = false; });
}
关键点:那个跑在别的 isolate 里的函数必须自包含——它拿不到 main isolate 的任何对象(不共享内存),所以别在里面 ref.read、别引用 context、别读全局单例。你要的东西全部通过参数传进去(会被复制),结果通过返回值传回来(也会被复制)。这个约束一开始有点别扭,但它正是「无并发 bug」的来源。
不是所有对象都能跨 isolate 传。能传的:基本类型、String、List/Map、以及大部分纯数据对象(比如你的 freezed 模型)。不能传的:带 BuildContext 的东西、打开的 socket/文件句柄、含闭包状态的对象、平台通道相关的对象。传了不能复制的会当场抛异常。经验:只在 isolate 边界传「纯数据」进去、传「纯数据」出来,把它当成一个纯函数用,就不会踩坑。
| Kotlin / JVM | Dart |
|---|---|
| 多个线程共享同一块内存堆 | 每个 isolate 独立内存堆 |
| 线程间直接读写同一对象 | 只能传消息,消息被复制 |
要 synchronized/Mutex/@Volatile 防竞争 | 不需要——没有共享就没有竞争 |
Dispatchers.Default 跑 CPU 重活 | Isolate.run { } |
| 线程池廉价,随便切 | isolate 较重,慎开、只为大计算开 |
这是一次真正的思维切换,不是换个 API 名。你在 JVM 上积累的「小心并发 bug」的经验——竞态、可见性、死锁——在 Dart 里大半用不上了,因为模型根本不共享内存。代价是并行的「粒度」变粗了(不能像开线程那样随手开、不能共享大对象),但对一个 App 来说,这个取舍几乎总是划算的:你失去的是很少用到的细粒度并行,换来的是「几乎不会写出并发 bug」。
这一章的一句话
Isolate 是 Dart 的真并行,但它不共享内存、传数据靠复制,更像多进程——所以用它换来了「几乎不会有并发 bug」;而你多半用不上它:等待型的活 await 就够,只有「会掉帧的纯计算、且数据量不大」才值得 Isolate.run 甩到另一个核上。
卷 V 到此结束——你已经能推理 Flutter 的时间维度了:为什么不卡、异步怎么写、重活往哪放。最后一卷,我们把前面所有东西拼成一个真的能交付的 App。下一章从导航开始:那个学校 App 有登录页、课表页、详情页,用户点来点去、还要能从通知直接跳进某节课——这就是路由要解决的问题。