Dart 线程模型和异步

上一篇介绍了 Dart 的基础语法,其中异步编程章节简单提到了 Future、async/await 和 Stream 的基本用法。但很多关键问题还没有展开:Dart 为什么是单线程?单线程怎么处理并发?事件循环到底怎么工作?Isolate 又是什么?它和线程有什么区别?本文把这些问题讲清楚。

一、Dart 的线程模型

1.1 单线程事件循环

Dart 的运行时基于事件循环(Event Loop)。所有 Dart 代码都运行在 isolate 中,默认运行在 main isolate 里。事件循环负责执行代码、收集和处理事件。

应用运行时,所有事件都被添加到事件队列(Event Queue)中。事件可以是 UI 重绘请求、用户点击、键盘输入、磁盘 I/O 等。因为应用无法预测事件的发生顺序,事件循环按入队顺序逐一处理:

// 伪代码
while (eventQueue.waitForEvent()) {
  eventQueue.processNextEvent();
}

这段伪代码来自 Dart 官方文档,它揭示了事件循环的核心:取出一个事件,处理它,再取下一个,循环往复。这个循环是同步的,运行在单个线程上。

这和 Kotlin 的模型有本质区别。Kotlin 依赖多线程 + 协程调度器(Dispatchers.Default、Dispatchers.IO 等),协程可以在不同线程间切换。而 Dart 的 async/await 全部运行在同一个 isolate 的同一个线程上,不会创建新线程,只是对事件循环的利用。

如果你有 Android 开发背景,可以把核心概念直接做个映射:

Android(Java/Kotlin/JVM) Flutter(Dart) 共同点 / 差异
Main 线程 + Looper/Handler Root Isolate + Event Loop 都是基于事件驱动的单线程死循环。Android 用 Looper 捞 Message;Dart 用 Event Loop 捞 Event
子线程(Thread / 线程池) 子 Isolate(Isolate.run / spawn) 都用于处理后台耗时任务。Android 共享堆内存,需处理各种锁;Dart 完全不共享内存,靠端口传数据
Kotlin 协程(Coroutines) Dart 异步(async / await) 都是非阻塞式的单线程异步(协作式)。底层都是把耗时等待切走,等结果返回再恢复执行

那单线程怎么处理耗时操作?答案是异步 API。当代码执行到异步操作(如网络请求)时,Dart 会将操作交给运行时或操作系统处理,同时注册一个回调,然后立即返回继续执行下一行代码。当异步操作完成后,结果作为事件放入队列,事件循环在合适的时候取出并执行回调。

1.2 事件队列与微任务队列

实际上,Dart 的事件循环维护两个队列:

  • 微任务队列(Microtask Queue):优先级极高,用于非常短的内部异步操作。
  • 事件队列(Event Queue):优先级较低,处理 I/O、手势、定时器、渲染等。

事件循环的调度规则是:先清空微任务队列中的所有任务,再从事件队列中取出一个事件处理。处理完一个事件后,再次检查微任务队列。这意味着微任务的优先级始终高于事件队列中的任务。

检查微任务队列是否有任务? ──(有)──> 执行微任务 ──> 循环检查直到清空
       ↓ (无)
检查事件队列是否有任务?   ──(有)──> 执行 1 个事件 ──> 返回顶部重新检查

如果你有 Android 经验,会联想到 Handler 和 MessageQueue。Dart 中的 Future(() => ...) 可以粗略类比为 Android 中写 Handler.post { ... },都是往队列扔一个任务。但有一个关键区别:Android 的 MessageQueue 本质上是单个队列,通过同步屏障(Sync Barrier)机制临时拦截同步消息、放行异步消息(用于优先处理 UI 渲染)。Dart 则是物理分离的两个队列,微任务队列在每次事件处理完毕后都会被完整清空,优先级保证更强。

scheduleMicrotask()Future.microtask() 可以将任务加入微任务队列:

import 'dart:async';

void main() {
  print('1'); // 同步执行

  Future(() => print('4')); // 进入事件队列

  scheduleMicrotask(() => print('3')); // 进入微任务队列

  print('2'); // 同步执行

  // main() 结束后,事件循环启动:
  // 先清空微任务队列 -> 打印 3
  // 再处理事件队列 -> 打印 4
}

// 输出:
// 1
// 2
// 3
// 4

Future(() => ...) 的回调进入事件队列,而 scheduleMicrotask 的回调进入微任务队列。虽然两者都是"异步"的(在 main 同步代码之后执行),但微任务先于事件队列任务执行。

微任务队列的存在使得 Dart 可以在处理完当前事件后、处理下一个事件前,优先完成一些"短小且紧急"的工作。日常开发中,你很少需要手动调度微任务,但理解它的存在对于排查 Future 执行顺序问题很重要。

1.3 async 函数的执行机制

上一篇已经介绍了 async/await 的基本用法,这里补充其底层执行机制。

官方文档明确指出:async 函数不会立即等待耗时操作完成,它只会执行到遇到的第一个 await 表达式,然后返回一个 Future。 只有当 await 表达式完成后,函数才会恢复执行。

Future<void> doWork() async {
  print('A - await 之前');      // 同步执行
  final data = await fetch();   // 遇到 await,函数返回 Future
  print('B - await 之后');      // fetch 完成后才执行
  final result = await parse(data); // 再次 await
  print('C - 完成');
}

Future<String> fetch() async {
  // 模拟网络请求
  await Future.delayed(Duration(seconds: 1));
  return 'raw data';
}

执行过程:

  1. 调用 doWork(),同步执行到 print('A')
  2. 遇到 await fetch()doWork 暂停,返回一个未完成的 Future<void>
  3. fetch() 内部遇到 await Future.delayed(...),同样暂停并返回 Future
  4. Dart 运行时将延迟任务交给底层处理,事件循环可以处理其他事件(如 UI 重绘、用户点击)
  5. 1 秒后延迟完成,事件循环恢复 fetch() 执行,返回 'raw data'
  6. doWork() 恢复执行,data 拿到 'raw data',继续执行到下一个 await

关键点:await 期间,Dart 代码暂停,当前 async 函数挂起,但不阻塞 Isolate 的执行流,事件循环可以继续处理其他事件。底层原生代码(如文件 I/O、网络请求)在操作系统层面执行,不占用 Dart 的事件循环。完成后将结果作为事件放入队列,事件循环再恢复对应的 async 函数。

顺带提一个常见的对应关系:Android 中延迟执行用 handler.postDelayed({ ... }, 1000),Dart 中用 Future.delayed(Duration(seconds: 1), () { ... }),后者同样是往事件队列扔一个延迟事件。

1.4 Zone -- 异步代码的执行上下文

前面讲了事件循环和两个队列,但还有一个概念没有提到:Zone。Zone 是 Dart 中异步代码的执行上下文,所有 Dart 代码都在某个 Zone 中运行。

你可以把 Zone 理解为一个"沙箱":它为其中的异步代码提供了一个隔离的运行环境,可以拦截和定制异步行为,最重要的是可以捕获该 Zone 内所有未处理的异步错误。

默认情况下,Dart 代码运行在 root zone 中。通过 runZoned() 可以创建子 Zone:

import 'dart:async';

void main() {
  runZoned(() {
    // 这段代码运行在新建的子 Zone 中
    throw Exception('Zone 内的异常');
  }, onError: (error, stackTrace) {
    // 捕获该 Zone 内未处理的异步错误
    print('捕获到错误: $error');
  });

  print('main 继续');
}

// 输出:
// 捕获到错误: Exception: Zone 内的异常
// main 继续

如果不使用 Zone,一个未捕获的异步异常会导致程序崩溃。Zone 的 onError 回调相当于给整段异步代码套了一层全局 try-catch。

Zone 还有一个重要特性:在某个 Zone 中注册的异步回调,无论何时执行,都运行在同一个 Zone 中。这意味着你在 runZoned 内部创建的 Future.then() 回调、Timer 回调等,即使被事件循环排到后面执行,也仍然属于创建它的那个 Zone。

void main() {
  runZoned(() {
    Timer(Duration(seconds: 1), () {
      throw Exception('定时器中的异常');
    });
  }, onError: (error, stackTrace) {
    print('1秒后捕获: $error');
  });

  print('main 继续执行,不阻塞');
}

// 输出:
// main 继续执行,不阻塞
// 1秒后捕获: Exception: 定时器中的异常

定时器回调在 1 秒后由事件循环触发,但它仍然运行在创建它的子 Zone 中,所以异常被该 Zone 的 onError 捕获,不会影响主 Zone。

Zone 还支持更细粒度的定制,通过 ZoneSpecification 可以拦截 print、定时器、微任务调度等底层操作:

void main() {
  runZoned(() {
    print('hello');
    Future(() => print('world'));
  }, zoneSpecification: ZoneSpecification(
    // 拦截 print 调用
    print: (self, parent, zone, line) {
      parent.print(zone, '[Zone] $line');
    },
  ));
}

// 输出:
// [Zone] hello
// [Zone] world

日常开发中你很少需要手动创建 Zone,但 Flutter 框架内部大量使用了 Zone:Flutter 在启动时通过 Zone 包裹整个应用,这样即使 Widget 中发生未捕获的异步异常,也不会直接崩溃,而是被框架的错误处理机制接管。如果你在 Flutter 中看到 FlutterError.onErrorPlatformDispatcher.instance.onError,背后就是 Zone 机制在起作用。

与 Android 对比:Zone 有点类似于 Android 中的 Thread.setDefaultUncaughtExceptionHandler,但 Zone 的能力更强 -- 它不仅能捕获异常,还能拦截和定制异步行为,且作用域是创建时的 Zone 而非全局。

二、Isolate -- Dart 的并发方案

2.1 为什么不用线程

现代设备都有多核 CPU。很多语言通过共享内存的多线程来利用多核,但共享状态并发容易出错:需要锁、信号量来保护共享数据,稍有不慎就会产生数据竞争(Data Race)和死锁。

Dart 选择了另一条路:Actor 模型。所有 Dart 代码运行在 isolate 中,每个 isolate 拥有独立的内存和自己的事件循环。isolate 之间不共享任何状态,只能通过消息传递通信。

这意味着 Dart 中 Isolate 之间不需要共享内存锁,没有竞态问题。因为没有共享状态,就不需要互斥锁;因为不共享内存,就不会出现一个线程修改另一个线程正在读取的数据。

需要注意的是,官方文档也指出:isolate 并不能完全消除竞态条件。如果你通过消息传递协调多个 isolate 的执行顺序,仍然可能出现逻辑层面的竞态。但内存级别的数据竞争在 Dart 中不会发生。

与 Kotlin 对比:Kotlin 的协程共享内存,多个协程可以访问同一份数据,需要通过 Mutex、Atomic 等机制保证线程安全。Dart 的 isolate 更接近 Kotlin 中的 Actor 模式实现(如 kotlinx.coroutines 的 actor),但隔离更加彻底。

在 Android 中,你在子线程修改 UI 会报 CalledFromWrongThreadException。在 Dart 中,因为 Isolate 之间内存完全隔离,子 Isolate 甚至连主 Isolate 的 UI 变量都"看不见",所以根本不存在跨线程改 UI 的可能。

2.2 Isolate 的生命周期

每个 isolate 的生命周期遵循一个简单的过程:

  1. 运行初始 Dart 代码(通常是 main() 函数)
  2. 初始代码可能注册事件监听器(响应用户输入、文件 I/O 等)
  3. 初始函数返回后,如果还有事件需要处理,isolate 继续存活
  4. 处理完所有事件后,isolate 退出

以 main isolate 为例:程序启动时执行 main()main() 执行完毕后,isolate 并不会立即退出,而是进入事件循环,等待和处理事件。UI 重绘、用户点击等事件都在 main() 退出之后才被处理。

如果一个同步操作耗时过长,事件队列中后续的事件就会被延迟处理。在客户端应用中,这表现为界面卡顿、动画掉帧,甚至完全无响应。这就是为什么耗时计算应该放到 worker isolate 中执行。

2.3 Isolate 不是线程

如果你从 Kotlin 等支持多线程的语言来到 Dart,可能会把 isolate 类比成线程,但官方文档明确强调:Isolate 不是线程

核心区别在于内存隔离。每个 isolate 拥有独立的全局字段,一个 isolate 中的状态对其他 isolate 完全不可见。举个例子:如果应用中有一个全局可变变量,在 spawned isolate 中修改它,主 isolate 中的同名变量不会受到任何影响,因为它们是各自 isolate 中的独立副本。

这与 Kotlin 的线程/协程完全不同。Kotlin 的多个协程运行在共享内存空间中,可以直接读写同一个变量。Dart 的 isolate 之间唯一的交互方式是消息传递。

2.4 Isolate Group 与性能

当 isolate 通过 Isolate.spawn() 创建新 isolate 时,两个 isolate 属于同一个 isolate group。同一 isolate group 内的 isolate 共享可执行代码,因此新 isolate 可以立即运行,启动开销较小。

Isolate.exit() 方法也只允许在同一 isolate group 内使用,它用于将结果直接发送回主 isolate 并终止 worker isolate。

某些特殊场景下可能需要 Isolate.spawnUri(),它从指定 URI 加载代码来创建新 isolate。但官方文档指出:spawnUri()spawn() 慢得多,且新 isolate 不在调用者的 isolate group 中,消息传递也更慢。

关于消息传递的性能:在同一 isolate group 内,isolate 之间可以高效地转移(transfer)内存,而非拷贝。Isolate.run() 的文档明确说明:worker isolate 将结果内存转移给主 isolate,而不是拷贝数据。这与 Web 平台的 Web Workers 不同,后者只能拷贝数据。

三、使用 Isolate

3.1 Isolate.run() -- 一次性任务

Isolate.run() 是最简单的 isolate 使用方式,它封装了完整的 worker isolate 生命周期:

  1. 创建并启动 isolate
  2. 在新 isolate 上运行函数
  3. 捕获结果
  4. 将结果返回给主 isolate
  5. 工作完成后终止 isolate
  6. 检查并捕获异常,抛回主 isolate

以下示例用 Isolate.run() 在 worker isolate 中解析 JSON:

import 'dart:convert';
import 'dart:io';
import 'dart:isolate';

const String filename = 'with_keys.json';

void main() async {
  final jsonData = await Isolate.run(_readAndParseJson);
  print('Number of JSON keys: ${jsonData.length}');
}

Future<Map<String, dynamic>> _readAndParseJson() async {
  final fileData = await File(filename).readAsString();
  final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
  return jsonData;
}

也可以直接传闭包,写法更像"并行执行"的控制流操作符:

void main() async {
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });
  print('Number of JSON keys: ${jsonData.length}');
}

两种写法效果相同。Isolate.run() 的返回值始终是 Future,因为主 isolate 的代码会继续执行,不会阻塞等待 worker isolate 完成。无论 worker 中执行的计算是同步还是异步,对主 isolate 来说都是并发执行的。

如果你使用 Flutter,可以直接用 compute() 函数,它是对 Isolate.run() 的封装,用法更简单,在后面的 3.4 节会详细介绍。

3.2 ReceivePort 与 SendPort

Isolate.run() 适用于一次性任务:spawn -> 计算 -> 返回 -> 退出。但如果需要反复执行相同计算,频繁 spawn 新 isolate 的开销不可忽视。这时需要创建长期存活的 isolate,通过端口进行双向通信。

ReceivePortSendPort 是 isolate 间通信的唯一方式:

  • ReceivePort:接收其他 isolate 发来的消息。它实现了 Stream,可以像流一样 listen
  • SendPort:向对应的 ReceivePort 发送消息,类似 StreamController

一个 SendPort 对应一个 ReceivePort,但一个 ReceivePort 可以有多个 SendPort。创建 ReceivePort 时会自动创建一个关联的 SendPort。

建立双向通信的过程:

  1. 主 isolate 创建 ReceivePort,将它的 SendPort 作为参数传给 worker isolate
  2. worker isolate 启动时收到这个 SendPort,创建自己的 ReceivePort
  3. worker isolate 通过收到的 SendPort 把自己的 SendPort 回传给主 isolate
  4. 双方各自 hold 对方的 SendPort,双向通道建立完成

这和 Kotlin 的 Channel 概念类似:Kotlin 用 Channel() 创建双向通道,Dart 用 ReceivePort/SendPort 对。但 Dart 的端口只能传递消息,不能共享对象引用。

3.3 长期通信:Isolate.spawn() + Ports

下面是一个完整的长期通信示例。主 isolate 创建一个 Worker,可以反复发送 JSON 字符串给 worker isolate 解析,worker 解析后回传结果。

为了处理多个并发请求的正确匹配,每条消息附带一个唯一 ID,配合 Completer 实现请求-响应对应:

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

class Worker {
  final SendPort _commands;       // 向 worker 发送消息的端口
  final ReceivePort _responses;   // 接收 worker 回复的端口
  final Map<int, Completer<Object?>> _activeRequests = {};
  int _idCounter = 0;
  bool _closed = false;

  Worker._(this._responses, this._commands) {
    _responses.listen(_handleResponsesFromIsolate);
  }

  // 创建 Worker 实例,spawn worker isolate 并建立双向通信
  static Future<Worker> spawn() async {
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();

    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };

    try {
      await Isolate.spawn(_startRemoteIsolate, initPort.sendPort);
    } on Object {
      initPort.close();
      rethrow;
    }

    final (ReceivePort receivePort, SendPort sendPort) =
        await connection.future;
    return Worker._(receivePort, sendPort);
  }

  // worker isolate 入口
  static void _startRemoteIsolate(SendPort sendPort) {
    final receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);
    _handleCommandsToIsolate(receivePort, sendPort);
  }

  // worker isolate 中处理来自主 isolate 的消息
  static void _handleCommandsToIsolate(
    ReceivePort receivePort,
    SendPort sendPort,
  ) {
    receivePort.listen((message) {
      if (message == 'shutdown') {
        receivePort.close();
        return;
      }
      final (int id, String jsonText) = message as (int, String);
      try {
        final jsonData = jsonDecode(jsonText);
        sendPort.send((id, jsonData));
      } catch (e) {
        sendPort.send((id, RemoteError(e.toString(), '')));
      }
    });
  }

  // 主 isolate 中处理来自 worker 的回复
  void _handleResponsesFromIsolate(dynamic message) {
    final (int id, Object? response) = message as (int, Object?);
    final completer = _activeRequests.remove(id)!;

    if (response is RemoteError) {
      completer.completeError(response);
    } else {
      completer.complete(response);
    }
  }

  // 公开方法:发送 JSON 字符串给 worker 解析
  Future<Object?> parseJson(String message) async {
    if (_closed) throw StateError('Closed');
    final completer = Completer<Object?>.sync();
    final id = _idCounter++;
    _activeRequests[id] = completer;
    _commands.send((id, message));
    return await completer.future;
  }

  // 关闭端口,释放资源
  void close() {
    if (!_closed) {
      _closed = true;
      _commands.send('shutdown');
      if (_activeRequests.isEmpty) _responses.close();
    }
  }
}

使用方式:

void main() async {
  final worker = await Worker.spawn();

  final result1 = await worker.parseJson('{"name": "Dart", "version": 3}');
  print(result1); // {name: Dart, version: 3}

  final result2 = await worker.parseJson('{"a": 1, "b": 2}');
  print(result2); // {a: 1, b: 2}

  worker.close();
}

这个 Worker 类的几个关键设计点:

  • RawReceivePort:用于接收 worker 的初始消息(SendPort),之后再转为普通 ReceivePort 以添加 listen 回调。普通 ReceivePort 只能有一个 listener,而 RawReceivePort 允许先处理初始消息再转换。
  • 消息 ID + Completer:每条消息附带唯一 ID,worker 回复时也带上 ID。主 isolate 用 Map<int, Completer> 保存待完成的请求,收到回复后根据 ID 找到对应的 Completer 并完成。这解决了多个并发请求时响应顺序不确定的问题。
  • shutdown 消息:通过约定一个特殊消息通知 worker 关闭端口,主 isolate 随后关闭自己的 ReceivePort。

3.4 何时使用 Isolate

官方文档没有硬性规定何时必须使用 isolate,但列出了以下适用场景:

  • 解析和解码特别大的 JSON 数据
  • 处理和压缩图片、音频、视频
  • 转换音频和视频文件
  • 对大型列表或文件系统执行复杂的搜索和过滤
  • 执行数据库等 I/O 操作
  • 处理大量网络请求

核心判断标准:如果计算量大到会暂时阻塞其他计算,就考虑用 isolate。 最常见的场景是 Flutter 应用中,某个耗时计算导致 UI 卡顿,这时应该把计算移到 worker isolate。

结合 Android 经验来区分两类场景:

场景 A:轻量级耗时(读本地文件、JSON 解析等 I/O 等待)

在 Android 中你可能会用 withContext(Dispatchers.IO)。在 Dart 中,直接用 async/await 即可,不需要开新的 Isolate。Dart 的 I/O 底层是由运行时的线程池实现的非阻塞操作,不会阻塞主 Isolate 的事件循环:

// 看起来是同步的,实际是非阻塞异步,不需要开新的 Isolate
final data = await file.readAsString();

场景 B:重度计算(大图滤镜、复杂算法、加密等 CPU 密集型)

在 Android 中你会开 Thread 或入线程池。在 Dart 中,你必须新开一个 Isolate,否则主 Event Loop 会被计算卡死导致界面掉帧(Jank)。

如果你使用 Flutter,推荐直接用 Flutter 封装好的 compute() 函数,它是对 Isolate.run() 的简易包装,会自动创建子 Isolate、计算完自动销毁并返回结果:

import 'package:flutter/foundation.dart';

// 1. 定义一个顶层函数或静态函数
//    (不能是闭包,不能持有 UI 状态,因为要跨 Isolate 传递)
int heavyCalculation(int count) {
  var result = 0;
  for (var i = 0; i < count; i++) {
    result += i;
  }
  return result;
}

// 2. 调用 compute,自动创建子 Isolate 执行
void main() async {
  final result = await compute(heavyCalculation, 1000000);
  print('结果: $result');
}

compute() 的回调必须是顶层函数或静态函数,因为 Isolate 之间不共享内存,闭包捕获的变量无法跨 Isolate 传递。

注意事项:

  • Flutter Web 不支持多 isolate,需要使用 Web Workers 替代
  • isolate 之间传递的消息有少数类型限制,如 SocketReceivePortDynamicLibraryPointer 等包含原生资源的对象不能发送
  • 标记了 @pragma('vm:isolate-unsendable') 的类实例也不能发送

四、异步编程进阶

4.1 Future API

上一篇介绍了 async/await 语法,它是处理 Future 的推荐方式。但在某些场景下(尤其是需要链式回调时),直接使用 Future API 更直观。

Future API 的三个核心方法:

Future<String> fetchName() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Dart';
}

void main() {
  fetchName()
      .then((name) {
        print('成功: $name');
        return name.toUpperCase();
      })
      .catchError((error) {
        print('失败: $error');
      })
      .whenComplete(() {
        print('无论成功失败都执行');
      });
}

// 1秒后输出:
// 成功: Dart
// 无论成功失败都执行

三个方法的对应关系:

Future API async/await 对应
then((value) => ...) final value = await future; 后续代码
catchError((error) => ...) try { ... } catch (e) { ... }
whenComplete(() => ...) try { ... } finally { ... }

来看一个具体对比,同样是异步读文件并 trim 结果,两种写法:

// 写法一:async/await
Future<String> _readFileAsync() async {
  final file = File(filename);
  final contents = await file.readAsString();
  return contents.trim();
}

// 写法二:Future API(then 链)
Future<String> _readFileAsync(String filename) {
  final file = File(filename);
  return file.readAsString().then((contents) {
    return contents.trim();
  });
}

两种写法功能完全等价,最终都返回一个 Future<String>。区别在于:

维度 async/await(写法一) Future API(写法二)
async 标记 函数标记为 async,return 值自动包装为 Future async 标记,直接返回已有的 Future
执行方式 await 暂停函数,等 Future 完成后恢复执行后续代码 then 注册回调,函数立即返回链式 Future
错误处理 try/catch/finally .catchError() / .whenComplete()
代码风格 接近同步代码,线性阅读 链式回调,适合简单的数据转换管道

写法一中,await 让代码看起来像同步的:读文件、拿到结果、trim、返回,一目了然。写法二中,readAsString() 返回一个 Future,.then() 注册回调,函数本身不"暂停",直接把这个链式 Future 返回给调用者。

有一个容易忽略的细节:async 函数中,即使 return 的是一个普通值(如 return contents.trim() 返回 String),Dart 也会自动将其包装为 Future<String>。而写法二没有 async 标记,必须确保函数返回的是 Future -- 这里 then() 的返回值本身就是 Future,所以没问题。

async/await 本质上是 then 链的语法糖,编译器会将 async/await 代码转换为等价的 Future 链式调用。选择哪种取决于代码结构:简单的顺序异步用 async/await 更清晰,多层链式转换用 Future API 更紧凑。

Future() 与 Future.value() 的执行顺序差异

理解了事件队列和微任务队列之后,来看一个容易让人困惑的问题:Future()Future.value() 创建的 Future,执行顺序不同。

void main() {
  print('1');

  Future(() => print('A'));       // ① 事件队列
  Future.value(() => print('B')); // ② 这里其实有坑,见下文

  print('2');
}

先说结论:Future() 的回调进入事件队列,而 Future.value() 创建的是一个已完成的 Future,后续通过 .then() 注册的回调进入微任务队列。所以微任务先执行。

但上面的代码有个陷阱:Future.value() 接收的参数是值本身,不是函数。Future.value(() => print('B')) 会把整个闭包作为值存下来,不会执行 print。正确的对比写法应该是:

void main() {
  print('1');

  Future(() => print('A'));              // ① 回调进入事件队列
  Future.value(null).then((_) => print('B')); // ② .then 回调进入微任务队列

  print('2');
}

// 输出:
// 1
// 2
// B
// A

B 先于 A 执行。原因:

  • Future(() => ...) 内部使用 Timer.run() 将回调排入事件队列
  • Future.value(v) 创建一个已经完成的 Future,当你在它上面调用 .then(cb) 时,Dart 不会立即执行 cb,而是将 cb 作为微任务排入微任务队列
  • 事件循环的规则是先清空微任务队列,再处理事件队列,所以 B 先于 A

同理,Future.microtask() 也会将回调排入微任务队列:

void main() {
  print('1');

  Future(() => print('A'));           // 事件队列
  Future.microtask(() => print('B')); // 微任务队列

  print('2');
}

// 输出:
// 1
// 2
// B
// A

总结三种 Future 构造方式对应的队列:

构造方式 回调进入的队列 执行优先级
Future(() => ...) 事件队列
Future.value(v).then(cb) 微任务队列
Future.microtask(() => ...) 微任务队列

这个差异在日常开发中很少需要关心,但当你遇到多个 Future 执行顺序不符合预期时,排查思路就是检查它们分别进入了哪个队列。

4.2 Completer

Completer<T> 允许你手动控制一个 Future 的完成。正常情况下,async 函数在 return 时自动完成 Future;但当你需要在事件回调中完成 Future 时,就需要 Completer。

基本用法:

import 'dart:async';

void main() {
  final completer = Completer<String>();

  // 模拟异步操作完成后调用 complete
  Future.delayed(Duration(seconds: 1), () {
    completer.complete('完成!');
  });

  completer.future.then((value) => print(value));
}

// 1秒后输出: 完成!

completer.complete(value) 完成 Future 并设置返回值,completer.completeError(error) 以错误完成 Future,completer.future 获取被控制的 Future。

在前面的 isolate Worker 示例中,Completer 发挥了关键作用:每次调用 parseJson() 时创建一个 Completer,将其 Future 返回给调用者。当 worker isolate 的响应到达时,通过 ID 找到对应的 Completer 并 complete,调用者的 await 随即返回。

在 Kotlin 中,类似的概念是 CompletableDeferred,同样用于手动控制异步结果的完成。

4.3 广播流(Broadcast Stream)

上一篇介绍了 Stream 是冷流:只有一个监听者,且只有在有消费者时才开始执行。但在实际开发中,有时需要多个监听者同时监听同一个流(Broadcast Stream)。Broadcast Stream属于热流。

创建广播流有两种方式:

方式一:StreamController.broadcast()

import 'dart:async';

void main() async {
  final controller = StreamController<int>.broadcast();

  controller.stream.listen((x) => print('监听器 A: $x'));
  controller.stream.listen((x) => print('监听器 B: $x'));

  controller.add(1);
  controller.add(2);

  await controller.close();
}

// 输出:
// 监听器 A: 1
// 监听器 B: 1
// 监听器 A: 2
// 监听器 B: 2

StreamController.broadcast() 创建的控制器,其 stream 允许多个监听者。每次 add() 的值会同时推送给所有监听者。

方式二:asBroadcastStream()

如果已经有一个普通(单订阅)Stream,可以调用 asBroadcastStream() 将其转为广播流:

void main() async {
  final coldStream = Stream.periodic(Duration(seconds: 1), (i) => i);
  final hotStream = coldStream.asBroadcastStream();

  hotStream.listen((x) => print('A: $x'));
  hotStream.listen((x) => print('B: $x'));
}

广播流和冷流的关键区别:

特性 单订阅流(冷流) 广播流(热流)
监听者数量 只能一个 可以多个
数据缓存 从头开始完整发送 后加入的监听者收不到已发送的数据
多次监听 每次 listen 重新执行生产逻辑 所有监听者共享同一个数据源

与 Kotlin 对比:

概念 Dart Kotlin
冷流 Stream(单订阅) Flow
热流 Stream.broadcast / asBroadcastStream() SharedFlow
带状态的热流 需手动管理 StateFlow
创建广播流 StreamController.broadcast() MutableSharedFlow()

一个重要区别:Dart 的广播流不缓存历史数据,新监听者只能收到加入之后的数据。Kotlin 的 SharedFlow 可以通过 replay 参数缓存指定数量的历史值,StateFlow 始终缓存最新值。如果需要类似 StateFlow 的行为,需要自行在 Dart 中维护当前值并在新监听者加入时推送。

4.4 理解 yield

上一篇介绍 Stream 时已经用到了 yield:在 async* 函数中,yield 向 Stream 产出一个值。这里深入理解它的工作机制。

yield 不是 returnreturn 会终止函数并返回最终结果;yield 产出当前值后,函数暂停而非终止,等待消费者读取下一个值时再恢复执行。这种"产出一个值就挂起"的机制,正是冷流懒加载特性的基础。

来看一个更完整的例子:

import 'dart:async';

Stream<int> naturalsTo(int n) async* {
  int k = 1;
  while (k <= n) {
    yield k;        // 产出 k,函数在此暂停
    k++;            // 消费者读取下一个值时,从这里恢复执行
  }
}

void main() async {
  await for (final value in naturalsTo(3)) {
    print(value);
  }
}

// 输出:
// 1
// 2
// 3

执行过程:

  1. naturalsTo(3) 被调用,返回一个 Stream(但函数体尚未执行)
  2. await for 开始消费,触发函数体执行
  3. k = 1,遇到 yield 1,产出值 1,函数暂停
  4. 消费者打印 1,请求下一个值
  5. 函数恢复执行,k++ 变为 2,yield 2 产出值 2,再次暂停
  6. 重复直到循环结束,Stream 关闭

还有一个 yield* 语法,用于产出另一个 Stream 的所有值(而非单个值):

Stream<int> expandedNaturals(int n) async* {
  yield* naturalsTo(n);     // 展开 naturalsTo(n) 的所有值
  yield* naturalsTo(n);     // 再展开一遍
}

void main() async {
  await for (final v in expandedNaturals(2)) {
    print(v);
  }
}

// 输出:
// 1
// 2
// 1
// 2

yield* 会把后面的 Stream 的所有值逐个转发给外层 Stream,相当于"展平"嵌套流。

与 Kotlin yield 的对比

Dart 和 Kotlin 都有 yield,但语义有重要区别:

维度 Dart yield Kotlin yield
适用场景 async* 生成器函数中,向 Stream 产出值 sequence 生成器中向消费者产出值;协程中用于让出执行权(不产出值)
挂起机制 产出值后函数暂停,消费者拉取下一个值时恢复 sequence 中挂起并产出值;协程中挂起让出调度权,不产出值
产出多个值 yield value 产出单个,yield* stream 展平另一个 Stream yield(value) 产出单个,yieldAll(sequence) 展平另一个 Sequence
类型标记 async* 标记函数,返回 Stream<T> sequence { } 构建器返回 Sequence<T>

对应的 Kotlin 代码:

// Kotlin: sequence + yield(产出值)
fun naturalsTo(n: Int): Sequence<Int> = sequence {
    var k = 1
    while (k <= n) {
        yield(k)       // 产出 k,挂起
        k++
    }
}

fun main() {
    naturalsTo(3).forEach { println(it) }
}

// Kotlin: Flow + emit (Flow 中用 emit 而非 yield)
suspend fun naturalsFlow(n: Int): Flow<Int> = flow {
    var k = 1
    while (k <= n) {
        emit(k)        // 产出 k,挂起
        k++
    }
}

注意 Kotlin 中的一个细节:yield 用于 sequence 生成器,而 Flow 中用的是 emit。Dart 中不区分这两者,统一用 yield。对比表:

操作 Dart Kotlin Sequence Kotlin Flow
产出单个值 yield value yield(value) emit(value)
展平另一个流 yield* stream yieldAll(sequence) emitAll(flow)
生成器标记 async* sequence { } flow { }

Kotlin 协程中的 yield()

除了 sequence 生成器,Kotlin 协程中还有一个无参的 yield(),它的语义完全不同:不产出任何值,只是挂起当前协程、让出执行权给调度器,允许其他协程运行。这是协作式多任务的手动让点:

// Kotlin: 协程中的 yield(),让出执行权
suspend fun task(id: String) {
    for (i in 1..3) {
        println("$id: $i")
        yield()   // 挂起,让其他协程有机会执行
    }
}

fun main() = runBlocking {
    launch { task("A") }
    launch { task("B") }
}

// 可能的输出(交替执行):
// A: 1
// B: 1
// A: 2
// B: 2
// A: 3
// B: 3

这个 yield() 和 Dart 的 yield 没有对应关系。Dart 是单线程事件循环,没有"让出执行权给其他协程"的概念。如果要类比,它更接近 Dart 中将一个微任务排入队列让事件循环先处理其他事件,但机制和目的都不同。

Dart 的 yield 专注于一个职责:向 Stream 产出值并挂起。Kotlin 的 yield 根据上下文有两种完全不同的语义:在 sequence 中产出值,在协程中让出调度权。这是两个语言在关键字复用上的重要差异。

本质上,Dart 的 yield 和 Kotlin sequence 中的 yield/Flow 中的 emit 做的是同一件事:产出值并挂起,等消费者准备好再恢复。只是 Dart 用一个关键字统一了 Kotlin 中两个不同场景的 API。而 Kotlin 协程中的无参 yield() 是另一回事,不要混淆。

五、总结

Dart 的并发体系有两层:

  • async/await + Future/Stream:运行在单个 isolate 的事件循环上,通过事件队列和微任务队列调度。适用于 I/O 密集型操作(网络请求、文件读写、定时器等),不会阻塞事件循环。
  • Isolate:真正的多核并行,每个 isolate 独立内存 + 独立事件循环,通过消息传递通信。适用于 CPU 密集型计算(JSON 解析、图片处理、复杂算法等),避免阻塞主 isolate 导致 UI 卡顿。

选择标准很简单:如果是 I/O 等待,用 async/await;如果是计算密集,用 Isolate。

与 Kotlin 的对比总结:

维度 Dart Kotlin
异步机制 事件循环 + async/await 协程 + suspend
并发模型 Isolate(独立内存,消息传递) 线程/协程(共享内存)
线程安全 天然隔离,无锁 需要 Mutex/Atomic 等机制
异步执行上下文 Zone(沙箱,可捕获异步错误) Thread.setDefaultUncaughtExceptionHandler
通道通信 SendPort/ReceivePort Channel
手动控制 Future Completer CompletableDeferred
产出值并挂起 yield / yield*(async* 中) yield(Sequence)/ emit(Flow)
冷流 Stream(单订阅) Flow
热流 Broadcast Stream SharedFlow/StateFlow

对于有 Kotlin 基础的开发者来说,Dart 异步的语法层面并不陌生(async/await、Stream 都有对应概念)。最需要适应的是 Isolate 的内存隔离模型:不能共享对象,只能传消息。这是 Dart 牺牲灵活性换取安全性的设计选择。

最后贴一张 Dart 实现模型图:

eventqueue

posted @ 2026-07-27 00:27  SharpCJ  阅读(4)  评论(0)    收藏  举报