Isolate 与 Event Loop
本节将简单介绍 Isolate 与 Event Loop。下一节将详细介绍如何使用 Isolate。
并发模型
不同于 Java 基于共享内存的多线程并发,Dart 采用了名为 Isolate 的隔离并发模型,其运行机制类似于轻量级进程。Isolate 之间采用“无共享”(Shared-Nothing)架构,通信完全依赖于异步消息传递。这种天然的隔离机制从根本上消除了数据竞争(Data Race),无需引入复杂的锁机制(Locks)即可确保运行时的内存安全。
graph TD
subgraph "Process (Java, C++) - 共享内存模型"
M[(Shared Heap Memory)]
T1([Thread 1]) --- M
T2([Thread 2]) --- M
T3([Thread 3]) --- M
style M fill:#f96,stroke:#333,stroke-width:2px
end
subgraph "Process (Dart) - 隔离模型"
subgraph I1 [Isolate 1]
H1[(Heap)] --- E1([Event Loop])
end
subgraph I2 [Isolate 2]
H2[(Heap)] --- E2([Event Loop])
end
I1 -.->|Message Passing| I2
style H1 fill:#6cf,stroke:#333
style H2 fill:#6cf,stroke:#333
end
Isolate 这种设计让 Dart 能够实现 “无停顿垃圾回收”。因为每个 Isolate 的 GC 是独立的,当子 Isolate 在后台清理垃圾时,不会阻塞主 Isolate 的 UI 渲染,这正是 Flutter 保持流畅(60/120 FPS)的底层秘诀之一。
Isolate
每一个 Isolate 到底初始化了什么? 当你 spawn 一个新的 Isolate 时,Dart VM 实际上做了以下工作:
- 分配独立的堆内存:用于存放该 Isolate 产生的对象。
- 创建Event Loop(事件循环):每个 Isolate 都有自己的事件队列和调度器。
- 创建调用栈(Stack):独立的执行上下文。
- 独立的垃圾回收器(Private GC):Isolate A 在进行 GC 时,不会停止 Isolate B 的运行(即没有全系统的 Stop-the-world)。
graph TD
%% 全局容器
subgraph Isolate_Internal ["Dart Isolate Instance"]
direction TB
%% 内存空间
subgraph Memory_Space ["私有内存堆 (Private Heap)"]
Objects([对象/实例])
GC[垃圾回收器]
end
%% 事件驱动核心
subgraph Event_System ["事件处理系统 (Event Loop System)"]
MQ@{shape: das, label: "微任务队列 Microtask Queue" }
EQ@{shape: das, label: "事件队列 Event Queue" }
Loop((⚙️ 事件循环 Event Loop))
end
%% 运行栈
Stack[[调用栈 Stack]]
%% 外部接口
subgraph Ports ["通信网关 (Communication)"]
RP[ReceivePort 接收端口]
SP[SendPort 发送端口]
end
end
%% 连接逻辑
Loop --> MQ
MQ -->|处理| Stack
Loop --> EQ
EQ -->|处理| Stack
Stack -->|修改/读取| Objects
%% 通信逻辑
External((外部其他 Isolate)) <-.->|消息传递/数据拷贝| Ports
RP -->|触发事件| EQ
%% 样式美化
style Isolate_Internal fill:#f5f5f5,stroke:#333,stroke-width:2px
style Memory_Space fill:#e3f2fd,stroke:#1565c0
style Event_System fill:#fff3e0,stroke:#e65100
style Ports fill:#f1f8e9,stroke:#33691e
style Loop fill:#673AB7,stroke:#311B92,stroke-width:2px,color:#fff
style MQ fill:#FFF9C4,stroke:#FBC02D
style EQ fill:#E1F5FE,stroke:#0288D1
style Stack fill:#4CAF50,stroke:#1B5E20,color:#fff
Isolate 生命周期
在 Dart 中,Isolate 的生命周期是一个闭环的异步过程。由于 Isolate 之间不共享内存,其状态切换高度依赖于 事件循环(Event Loop)和通信端口(Port)。
stateDiagram-v2
[*] --> Spawning: Isolate.spawn()
state Spawning {
[*] --> Allocating: 分配独立堆内存
Allocating --> Loading: 加载代码与入口函数
}
Spawning --> Running: entryPoint(入口函数)开始执行
state Running {
[*] --> EventLoop
EventLoop --> MicrotaskQueue: 处理微任务
MicrotaskQueue --> EventQueue: 处理事件/消息
EventQueue --> EventLoop: 循环往复
state "Active (Listening)" as Active
EventLoop --> Active: ReceivePort 开启
Active --> EventLoop: 收到消息
}
Running --> Exiting: entryPoint执行完毕 && 无活跃端口
Running --> Exiting: 收到 kill 信号
Running --> ErrorState: 未捕获的异常
ErrorState --> Exiting: 触发 ErrorListener
Exiting --> [*]: 资源回收/内存释放
Event Loop
Isolate与Event Loop是密不可分的,每个Isolate都有唯一的一个Event Loop与之对应。Isolate 是物理边界,而 Event Loop 是运行逻辑。
graph TD
subgraph Core [" "]
direction TB
EL((⚙️ Event Loop))
end
MTQ@{shape: das, label: "Microtask Queue" }
EQ@{shape: das, label: "Event Queue" }
Exec[[当前执行上下文]]
EL ==> |轮询并清空| MTQ
MTQ -->|弹出任务| Exec
Exec -.->| microtask | MTQ
Exec -.->| event | EQ
EL ==>|检查并取出一个| EQ
EQ -->|弹出任务| Exec
Input([外部事件: I/O, Click, Timer]) ---> |进入队列| EQ
style EL fill:#673AB7,stroke:#311B92,stroke-width:4px,color:#fff
style Exec fill:#4CAF50,stroke:#1B5E20,color:#fff
style MTQ fill:#FFF9C4,stroke:#FBC02D
style EQ fill:#E1F5FE,stroke:#0288D1
style Core fill:none,stroke:none
Event Loop就像一个不停转动的时间齿轮, 同时维护着两个优先级不同的队列:
-
Event Queue(事件队列):优先级较低。包含外部事件,如 I/O、计时器(Timer)、鼠标点击、绘制事件等。
-
Microtask Queue(微任务队列):优先级最高。通常用于非常简短的、需要异步执行的操作。只要微任务队列不为空,Event Loop 就会一直执行它,直到清空。 注意:如果在微任务中不断产生新的微任务,事件队列就会被“饿死”,导致 UI 无法响应。
Event Queue 保证了程序的响应性,它让 UI 刷新和用户点击有条不紊地排队。而 Microtask Queue 则保证了异步逻辑的原子性。当一个 Future 完成时,我们希望它的后续处理(then)能尽快执行,甚至赶在下一个用户点击之前。微任务队列就是为了这种‘插队’需求而生的,它让异步逻辑在宏观上看起来像是连续执行的。
再谈 asyc/wait
在 Dart 中,async/await 并不是让代码进入了另一个线程,而是将一个长函数拆分成了多个逻辑片段,并利用 Event Loop 重新排队。下面结合示例进行分析。
// ex741.dart
void main() async {
print('main start');
var result = await task();
print(result);
print('main end');
/* Output:
main start
task start
task done
main end
*/
}
Future<String> task() async {
await Future(() => print('task start'));
return 'task done';
}
sequenceDiagram
autonumber
participant M as main
participant EL as ⚙️ Event Loop
participant MTQ as Microtask Queue (MTQ)
participant EQ as Event Queue
participant Exec as Execution Context
Note over M, Exec: 同步启动阶段
M->>Exec: print('main start')
M->>Exec: 调用 task()
Note right of Exec: 进入 task 内部
Exec->>EQ: 注册 Future 任务 (print 'task start')
Exec-->>M: task 函数挂起 (await Future)
Note over M, Exec: 控制权回归 main
M-->>EL: main 函数挂起 (await task)
Note over EL: 齿轮旋转 - 处理宏任务
EL->>EQ: 提取并执行 Future
EQ->>Exec: print('task start')
Exec->>MTQ: Future 完成,恢复 task 剩余部分
Note over EL: 齿轮旋转 - 优先调度 (MTQ)
EL->>MTQ: 提取任务:完成 task 并返回 'task done'
MTQ->>Exec: 执行 return 'task done'
Exec->>MTQ: task 完成,恢复 main 剩余部分
Note over EL: 再次清空微任务
EL->>MTQ: 提取任务:打印结果
MTQ->>Exec: print('task done')
MTQ->>Exec: print('main end')
Note over Exec: 所有任务链结束
上述时序图中的三个关键“跳转”:
-
a. 在 task() 中,一遇到
await Future(...),task函数就会立即交出控制权。此时并没有立即出现task start,因为task start被丢进了 Event Queue。 -
b. 双重挂起: 此时内存中有两个处于“暂停”状态的函数:
main在等task,task在等 Future。这种嵌套挂起展示了 Dart 异步非阻塞的本质。 -
c. 微任务的“接力”: 当 Future(宏任务)执行完后,它并没有直接回到 main。它先触发了 task 的恢复(微任务),
task完成后又触发了main的恢复(又一个微任务)。这就是为什么 Microtask Queue 被称为异步链条的“粘合剂”。
sleep() VS await Future.delayed()
sleep()是一个同步阻塞(block)调用,它将强行卡死EventLoop时间齿轮,霸占 CPU 并原地停止。
await Future.delayed(),是一个异步非阻塞操作,就像执行区里的工人看了一眼闹钟,发现时间没到,于是主动离开位子,把执行区让出来,自己去休息室等候。其过程是:
Future.delayed向底层操作系统注册一个计时器await关键字将当前函数挂起(Suspend)- 执行区立即变空,Event Loop 齿轮可以自由转动,去处理屏幕刷新、点击等其他队列任务。
- 时间一到,计时器把“恢复执行”的任务丢进 Event Queue,等齿轮下一轮转过来时执行。
| 特性 | sleep(...) | await Future.delayed(...) |
|---|---|---|
| 执行性质 | 同步、阻塞 | 异步、挂起(非阻塞) |
| 对EventLoop齿轮影响 | 停止转动 | 正常转动 |
| 队列任务 | 被饿死(Starvation) | 正常调度 |
| 用户体验 | 应用卡死,无法操作 | 应用流畅,后台等待 |
| 底层实现 | 操作系统线程休眠 | 计时器事件注册 + 任务回流 |
Async 还是 Isolate ?
虽然 async/await 能处理异步,但它本质上还是在同一个线程的 Event Loop 里“排队”。如果你需要处理 CPU 密集型任务(如大图片滤镜、复杂加解密、解析 100MB 的 JSON),主线程依然会卡顿,这时就需要启动一个新的 Isolate 进行并行处理。
| 场景 | 推荐方案 | 底层逻辑 |
|---|---|---|
| 网络请求 (HTTP) | async/await | 线程在等待 IO 响应,不需要额外 CPU 计算。 |
| 文件读写 (I/O) | async/await | 由操作系统异步处理,返回后在 Event Loop 排队。 |
| JSON 解析 (巨大) | Isolate | 解析需要消耗大量 CPU 时间,会阻塞 UI 渲染。 |
| 图像/视频处理 | Isolate | 属于高强度计算,必须移出 Main Isolate。 |
Reference
-
https://api.dart.dev/dart-isolate/Isolate-class.html