跳转至正文

并发与 isolate

在 Flutter 中使用 Dart isolate 实现多线程。

所有 Dart 代码在 isolate 中运行,类似线程,但各有独立内存,不以任何方式共享状态,只能通过消息通信。默认情况下 Flutter 应用的所有工作都在单个 isolate——主 isolate——上完成。多数情况下该模型使编程更简单,且足够快,应用 UI 不会无响应。

但有时应用需要执行特别大的计算,可能导致「UI jank」(卡顿)。若因此出现 jank,可将计算移到辅助 isolate,让运行时环境与主 UI isolate 的工作并发执行,并利用多核设备。

每个 isolate 有独立内存和事件循环。事件循环按加入事件队列的顺序处理事件。在主 isolate 上,这些事件可以是处理 UI 点击、执行函数或在屏幕上绘制一帧等。

下图展示了一个示例事件队列,其中有 3 个事件等待处理。

The main isolate diagram

为流畅渲染,Flutter 每秒向事件队列添加 60 次「paint frame」事件(60Hz 设备)。若这些事件未及时处理,应用会出现 UI jank,甚至更糟——完全无响应。

Event jank diagram

若某过程无法在帧间隔(两帧之间的时间)内完成,最好将工作卸载到另一 isolate,确保主 isolate 每秒产出 60 帧。在 Dart 中 spawn isolate 时,它可与主 isolate 并发处理工作而不阻塞主 isolate。

有关 isolate 与事件循环的更多说明,请参阅 Dart 文档的 并发页面

Watch on YouTube in a new tab: "Isolates and the event loop | Flutter in Focus"

isolate 的常见用例

#

何时应使用 isolate 只有一条硬性规则:大型计算导致 Flutter 应用出现 UI jank。当任何计算耗时超过 Flutter 的帧间隔时就会出现 jank。

Event jank diagram

任何过程都 可能 耗时更长,具体取决于实现和输入数据,因此无法详尽列出何时需要考虑使用 isolate。

此外,isolate 常用于以下场景:

  • 从本地数据库读取数据

  • 发送推送通知

  • 解析和解码大型数据文件

  • 处理或压缩照片、音频和视频文件

  • 转换音频和视频文件

  • 使用 FFI 时需要异步支持

  • 对复杂列表或文件系统应用过滤

isolate 之间的消息传递

#

Dart 的 isolate 是 Actor 模型 的实现,只能通过 Port 对象 进行消息传递通信。消息在 isolate 之间「传递」时,通常从发送 isolate 复制到接收 isolate,因此传给 isolate 的值即使在该 isolate 上被修改,也不会改变原 isolate 上的值。

传给 isolate 时 不复制的对象 仅包括不可变对象,如 String 或不可修改的字节。传递不可变对象时,为提升性能会发送引用而非复制对象。因不可变对象无法更新,这有效保持了 Actor 模型行为。

例外是使用 Isolate.exit 发送消息时 isolate 退出:因发送 isolate 发送后不再存在,可将消息所有权从一个 isolate 转给另一个,确保只有一个 isolate 能访问该消息。

发送消息的两种底层原语是 SendPort.send(发送时复制可变消息)和 Isolate.exit(发送消息引用)。 Isolate.runcompute 底层都使用 Isolate.exit

短期 isolate

#

在 Flutter 中将过程移到 isolate 的最简单方式是使用 Isolate.run。该方法 spawn isolate,向 spawn 的 isolate 传递回调以开始计算,返回计算结果,计算完成后关闭 isolate。这一切与主 isolate 并发进行,不会阻塞主 isolate。

Isolate diagram

Isolate.run 需要一个参数:在新 isolate 上运行的回调函数。该回调的函数签名必须恰好有一个必需的无名参数。计算完成后,将回调的返回值返回主 isolate 并退出 spawn 的 isolate。

例如,以下代码从文件加载大型 JSON 并转换为自定义 Dart 对象。若 JSON 解码未卸载到新 isolate,该方法会使 UI 数秒无响应。

dart
// Produces a list of 211,640 photo objects.
// (The JSON file is ~20MB.)
Future<List<Photo>> getPhotos() async {
  final String jsonString = await rootBundle.loadString('assets/photos.json');
  final List<Photo> photos = await Isolate.run<List<Photo>>(() {
    final List<Object?> photoData = jsonDecode(jsonString) as List<Object?>;
    return photoData.cast<Map<String, Object?>>().map(Photo.fromJson).toList();
  });
  return photos;
}

有关在后台使用 Isolate 解析 JSON 的完整 walkthrough,请参阅 这篇 cookbook

有状态的长期 isolate

#

短期 isolate 使用方便,但 spawn 新 isolate 和在 isolate 间复制对象有性能开销。若反复用 Isolate.run 做相同计算,创建不立即退出的 isolate 可能性能更好。

为此可使用 Isolate.run 所封装的一些底层 isolate API:

使用 Isolate.run 时,新 isolate 在向主 isolate 返回单条消息后立即关闭。有时你需要长期存活、可随时间互发多条消息的 isolate。在 Dart 中可用 Isolate API 和 Port 实现,这些长期 isolate 俗称 background workers(后台 worker)。

长期 isolate 适用于需要在应用生命周期内反复运行的特定过程,或在一段时间内运行并需向主 isolate 产生多个返回值的过程。

或者,你可以使用 worker_manager 管理长期 isolate。

ReceivePort 与 SendPort

#

用两个类(除 Isolate 外)建立 isolate 间的长期通信: ReceivePortSendPort。这些 port 是 isolate 之间相互通信的唯一方式。

Port 的行为类似 Stream:在一个 isolate 中创建 StreamControllerSink,在另一个 isolate 中设置监听器。类比中 StreamController 称为 SendPort,可用 send()「添加」消息; ReceivePort 是监听器,收到新消息时用消息作为参数调用提供的回调。

有关主 isolate 与 worker isolate 之间双向通信的深入说明,请参阅 Dart 文档 中的示例。

在 isolate 中使用平台插件

#

可在后台 isolate 中使用平台插件,使插件将繁重的平台相关计算卸载到不阻塞 UI 的 isolate。例如使用原生宿主 API 加密数据时,以前 编组数据 到宿主平台可能占用 UI 线程时间,现可在后台 isolate 完成。

平台通道 isolate 使用 BackgroundIsolateBinaryMessenger API。以下片段展示在后台 isolate 中使用 shared_preferences package 的示例。

dart
import 'dart:isolate';

import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  // Identify the root isolate to pass to the background isolate.
  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
  Isolate.spawn(_isolateMain, rootIsolateToken);
}

Future<void> _isolateMain(RootIsolateToken rootIsolateToken) async {
  // Register the background isolate with the root isolate.
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);

  // You can now use the shared_preferences plugin.
  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();

  print(sharedPreferences.getBool('isDebug'));
}

Isolate 的限制

#

若你来自支持多线程的语言,可能预期 isolate 像线程一样工作,但事实并非如此。 isolate 有独立全局字段,只能通过消息传递通信,确保可变对象仅在一个 isolate 中可访问,因此受限于自身内存访问。例如应用有全局可变变量 configuration,spawn 的 isolate 会复制为新全局字段;在 spawn 的 isolate 中修改该变量,主 isolate 中不变,即使将 configuration 作为消息传给新 isolate 亦然。这是 isolate 的设计行为,考虑使用 isolate 时需牢记。

Web 平台与 compute

#

包括 Flutter web 在内的 Dart Web 平台不支持 isolate。若 Flutter 应用面向 Web,可使用 compute 确保代码能编译。 compute() 在 Web 上于主线程运行计算,在移动设备上 spawn 新线程。在移动和桌面平台上 await compute(fun, message) 等价于 await Isolate.run(() => fun(message))

有关 Web 并发的更多信息,请参阅 dart.dev 的 并发文档

无法访问 rootBundledart:ui 方法

#

所有 UI 任务和 Flutter 本身都与主 isolate 绑定,因此无法在 spawn 的 isolate 中用 rootBundle 访问资源,也不能在 spawn 的 isolate 中执行 widget 或 UI 工作。

从宿主平台到 Flutter 的插件消息受限

#

通过后台 isolate 平台通道,可在 isolate 中使用平台通道向宿主平台(如 Android 或 iOS)发送消息并接收响应,但无法接收来自宿主平台的主动消息。

例如,无法在后台 isolate 中设置长期 Firestore 监听器,因为 Firestore 通过平台通道向 Flutter 推送主动更新。但可在后台查询 Firestore 获取响应。

更多信息

#

有关 isolate 的更多信息,请参阅以下资源:

  • 若使用多个 isolate,可考虑 Flutter 的 IsolateNameServer 类,或为非 Flutter 的 Dart 应用使用复制该功能的 pub package。

  • Dart 的 Isolate 是 Actor 模型 的实现。

  • isolate_agents 是抽象 Port、便于创建长期 isolate 的 package。

  • 阅读 BackgroundIsolateBinaryMessenger API 公告 的更多内容。