GEOZ

Dart跨Isolate共享数据:shared_map把150行管道代码砍成几行

2026/9/22
Dart跨Isolate共享数据:shared_map把150行管道代码砍成几行

AIAI Summary (BLUF)

Dart的Isolate采用无共享内存模型,跨Isolate共享数据通常需要繁琐的序列化或消息传递。shared_map包提供了一种优雅的解决方案,让你像操作普通Map一样在多个Isolate间同步读写数据。本文通过完整示例演示其核心用法,包括SharedMap、SharedMapCached和SharedStore。

核心洞察

Dart 的 isolate 不共享内存,这个设计消灭了数据竞争,但也让跨 isolate 共享缓存变成了一件麻烦事。shared_map 这个包用一套引用传递机制把端口通信的脏活全包了,代码量能砍掉一大半。我唯一担心的是它的性能边界,高频读写场景下跨 isolate 消息派发的开销到底有多大,文章里没给 benchmark,你自己压测一下比较稳妥。


这是 Dart 和 Flutter 系列的第三篇,每篇独立成文,不用担心没看过前两篇。

Dart 的并发模型建立在 Isolate 之上。和 Java、C++、Go 里的线程不同,Dart 的 isolate 之间不共享内存。每个 isolate 有自己的私有堆,也有自己的单线程事件循环。

这种不共享的设计很聪明。数据竞争、死锁、互斥锁争用、线程同步的坑,统统不存在。

但问题来了,你总会有需要跨 isolate 共享数据的时候。

想象一个常见的生产场景:

你在做一个 Flutter 应用,需要在后台跑重活,比如批量压缩图片、解析巨大的 JSON、算加密哈希、跑复杂的机器学习计算。为了让 UI 稳定在 120 帧,你用 Isolate.run 把这些活丢给后台 isolate。

现在假设这些并发后台任务都需要访问一个共享的内存缓存,比如已解析的元数据、认证 token、或者共享的计算结果,用来避免重复劳动。

在 Dart 里你怎么做?

以前你只有两个不太好的选择:

  1. 每次跨 isolate 边界都把整个数据结构序列化再拷贝一份。数据量大或者操作频繁的话,CPU 烧得厉害,GC 压力也大。
  2. 自己用 ReceivePortSendPort 搭一个消息传递服务器。你得自己设计请求响应的 DTO,生成唯一的请求关联 ID,接上响应 completer,写 150 行脆弱的管道代码,就为了做一次简单的键值查询。

还有第三个更好的选择,但几乎没人提:package:shared_map

核心结论

  1. shared_map 是一个零依赖的纯 Dart 包,在 pub.dev 上获得 160/160 满分评分,完全兼容 Dart 3,支持 iOS、Android、macOS、Windows、Linux、Web 及服务端等所有 Dart 可运行平台。

  2. 该包通过"引用模式"实现跨 isolate 数据共享:主 isolate 创建 SharedMap 作为权威数据源,调用 .sharedReference() 生成轻量可序列化令牌,工作 isolate 通过 SharedMap.fromSharedReference(ref) 重建代理实例,所有读写操作自动派发回主实例并同步到所有 isolate。

  3. 使用 shared_map 可完全避免手写 SendPortReceivePortCompleter 及序列化样板代码,官方示例中两个后台 isolate 的跨 isolate 读写与同步仅需数行代码即可完成。

  4. shared_map 内置 SharedMapCached 子类,可在工作 isolate 本地堆缓存已查询的键值,在超时窗口内(如 30 秒)的后续读取直接本地返回,避免高频读取场景下每次 get() 都触发跨 isolate 消息派发。

  5. shared_map 适用于 CPU 密集型任务协调、内存去重与缓存、跨 isolate 指标计数等场景;但不适用于持久化磁盘存储(应使用 SQLite、Drift 等)和单 isolate 应用(普通 Map<K, V> 即可满足需求)。

shared_map 是什么

这个包由资深 Dart 工程师 Graciliano M. Passos 开发,提供了一个同步的 Map 数据结构,专门用来跨 Dart isolate 和异步工作流共享数据。

几个让它值得关注的点:

  • 零依赖:纯 Dart,没有任何第三方依赖。
  • Pub 评分 160/160:pub.dev 上满分,完全兼容 Dart 3。
  • 全平台支持:Dart 能跑的地方它就能跑,iOS、Android、macOS、Windows、Linux、Web、后端 CLI 和服务端都行。
  • 熟悉的 Map 语义:用标准的异步键值方法操作,get()put()putIfAbsent()update() 这些。

你不需要手动管理端口,shared_map 在底层透明地处理跨 isolate 通信协议。

工作原理:引用模式

shared_map 的核心心智模型很简单:

  1. 主实例:在主 isolate 上创建一个 SharedMap,比如 Flutter UI 线程或者服务端主循环。这个实例是权威数据源。
  2. 共享引用:调用 .sharedReference() 生成一个轻量、可序列化的令牌。
  3. 辅助实例:把这个轻量令牌传过 isolate 边界,比如传进 Isolate.run。在 isolate 内部用 SharedMap.fromSharedReference(ref) 重建一个代理实例。

工作 isolate 做的任何读取、写入、修改都会自动派发回主实例,并同步到所有 isolate。

实际跑一遍

写一个完整的独立示例。模拟多个并发工作 isolate 处理数据,从共享缓存读取,顺便往缓存里写新条目:

import 'dart:isolate';
import 'package:shared_map/shared_map.dart';

void main() async {
  // 1. 在主 isolate 上创建 SharedStore 和 SharedMap
  final store = SharedStore('app_cache');
  final userCache = await store.getSharedMap<String, String>('users');

  // 预置一个初始值
  await userCache!.put('user_101', 'Randal (Admin)');

  // 2. 提取轻量、可序列化的引用
  final cacheReference = userCache.sharedReference();

  print('--- 启动后台 Worker 1 ---');

  // 3. 把引用传进后台 isolate
  final worker1Result = await Isolate.run(() async {
    // 重建同步 map 代理
    final workerMap = SharedMap<String, String>.fromSharedReference(cacheReference);

    // 读取主 isolate 之前存的值
    final user = await workerMap.get('user_101');
    print('[Worker 1] 从共享缓存读取: $user');

    // 从后台 worker 往共享缓存写入新值
    await workerMap.put('user_102', 'Wilhelm (Engineer)');
    return 'Worker 1 完成';
  });

  print(worker1Result);

  print('--- 启动后台 Worker 2 ---');

  // 4. 启动第二个 isolate,验证跨 isolate 同步
  final worker2Result = await Isolate.run(() async {
    final workerMap = SharedMap<String, String>.fromSharedReference(cacheReference);

    // Worker 2 能立刻读到 Worker 1 刚写入的值
    final user102 = await workerMap.get('user_102');
    print('[Worker 2] 读取 Worker 1 写入的值: $user102');

    // 原子地使用 putIfAbsent
    final user103 = await workerMap.putIfAbsent('user_103', 'Guest User');
    return '[Worker 2] 添加了: $user103';
  });

  print(worker2Result);

  // 5. 验证主 isolate 反映了所有更新
  print('--- 回到主 Isolate ---');
  print('缓存总条目数: ${await userCache.length()}');
  print('主 isolate 上的 user_102: ${await userCache.get('user_102')}');
  print('主 isolate 上的 user_103: ${await userCache.get('user_103')}');
}

控制台输出

--- 启动后台 Worker 1 ---
[Worker 1] 从共享缓存读取: Randal (Admin)
Worker 1 完成
--- 启动后台 Worker 2 ---
[Worker 2] 读取 Worker 1 写入的值: Wilhelm (Engineer)
[Worker 2] 添加了: Guest User
--- 回到主 Isolate ---
缓存总条目数: 3
主 isolate 上的 user_102: Wilhelm (Engineer)
主 isolate 上的 user_103: Guest User

注意刚才发生了什么:

  • 两个独立的后台 isolate 互相通信,并把数据共享回了主线程。
  • 一行 SendPortReceivePortCompleter 或序列化样板代码都没写。

进阶能力:用 SharedMapCached 做本地读缓存

如果你的工作 isolate 要做几千次快速读取,你可能不希望每次 get() 都走一次跨 isolate 消息派发。

shared_map 内置了一个子类叫 SharedMapCached

final cachedWorkerMap = SharedMapCached<String, String>.fromSharedReference(
  cacheReference,
  // 在本 isolate 本地缓存条目,用于高吞吐读取
  timeout: const Duration(seconds: 30),
);

SharedMapCached 查询已存在的键时,它会把值缓存在工作 isolate 的堆里。如果后续读取发生在超时窗口内,直接本地返回,没有跨 isolate 延迟。

SharedStore 分组管理 Map

复杂应用里你很少只有一个缓存。可能有:

  • 图片缓存(SharedMap<String, Uint8List>
  • 用户资料缓存(SharedMap<String, UserProfile>
  • 限流器 map(SharedMap<String, int>

与其到处传几十个独立的引用,不如传一个 SharedStoreReference

// 主线程:
final store = SharedStore('global_store');
await store.getSharedMap<String, String>('tokens');
await store.getSharedMap<String, int>('rate_limits');

final storeRef = store.sharedReference();

// 在任意后台 isolate 里:
await Isolate.run(() async {
  final workerStore = SharedStore.fromSharedReference(storeRef);

  // 动态解析这个 store 下注册的任意 map
  final tokens = await workerStore.getSharedMap<String, String>('tokens');
  final rateLimits = await workerStore.getSharedMap<String, int>('rate_limits');

  // ...
});

什么时候该用,什么时候不该用

任何工具都有它的适用边界。

适合用 shared_map 的场景:

  1. CPU 密集型任务的协调:跑一批后台 isolate(Isolate.run 或常驻 worker isolate),需要共享查询。
  2. 内存去重和缓存:防止并发 worker 重复计算或下载同一个资源。
  3. 跨 isolate 指标和计数器:收集统计、遥测数据,或者跨线程的限流令牌。

不适合用 shared_map 的场景:

  1. 持久化磁盘存储:shared_map 是内存数据结构。数据需要跨应用重启存活的话,用 SQLite、Drift 或者持久化键值存储。
  2. 单 isolate 应用:所有代码都跑在根 UI isolate 上的话,一个普通的 Dart Map<K, V> 或者响应式 Signal 就够了,没必要为异步抽象付出代价。

总结

Dart 的 isolate 架构让我们的代码远离并发 bug,但你不该为了跨 worker 任务共享一个内存缓存就写几百行端口管道代码。

引入 shared_map 之后:

  1. Isolate 保持隔离,UI 线程保持响应。
  2. 不用再写 SendPortReceivePort 的意大利面条代码。
  3. 得到原子、同步的键值存储,零外部依赖。

pubspec.yaml 里加上 shared_map: ^1.1.9,别再从头造 isolate 消息传递的轮子了。


Randal L. Schwartz 是 Dart 和 Flutter 的 Google Developer Expert(GDE),资深软件架构师。

  • 在 YouTube 看深度讲解和实战演示:@RandalOnDartAndFlutter
  • 在 GitHub 上联系:@RandalSchwartz

常见问题(FAQ)

shared_map 和普通 Map 有什么区别?

shared_map 提供同步的 Map 数据结构,专为跨 Dart isolate 共享数据设计。它通过引用传递机制透明处理端口通信,让你像操作普通 Map 一样使用 get、put 等方法,无需手动序列化或管理 SendPort/ReceivePort。

shared_map 的性能怎么样?高频读写场景下会不会很慢?

shared_map 在高频读写时可能因跨 isolate 消息派发产生开销。文章未提供 benchmark,建议自行压测。对于高吞吐读取,可使用 SharedMapCached 在本地缓存条目,减少跨 isolate 通信次数。

什么时候该用 shared_map?什么时候不该用?

适合需要跨 isolate 共享缓存、避免重复计算的场景,如后台任务共享元数据或认证 token。不适合对性能极度敏感的高频读写,或数据量极大且可容忍序列化成本的场景。建议先评估性能边界。

Roger深圳
本文由 Roger 审核,最后更新于 2026年9月22日
联系编辑 →
← 返回文章列表
分享到:微博

版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。

文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。

若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。