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 在等 tasktask 在等 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