news 2026/3/1 8:09:41

Android学Dart学习笔记第二十六节 并发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android学Dart学习笔记第二十六节 并发

概览

本文包含了 Dart 中并发编程工作原理的概念性概述。它从较高层面解释了事件循环、异步语言特性和隔离区。

Dart 中的并发编程既指异步 API(如 Future 和 Stream),也指隔离区,隔离区允许你将进程转移到独立的核心上。

所有 Dart 代码都在隔离区中运行,从默认的主隔离区开始,并且可以选择性地扩展到你显式创建的任何后续隔离区。当你生成一个新的隔离区时,它拥有自己独立的内存和自己的事件循环。事件循环是 Dart 中实现异步和并发编程的关键。

事件循环

Dart 的运行时模型基于事件循环。事件循环负责执行程序代码、收集和处理事件等。

当你的应用程序运行时,所有事件都会被添加到一个名为事件队列的队列中。事件可以是任何事情,从重新绘制用户界面的请求,到用户的点击和按键操作,再到来自磁盘的输入 / 输出。由于你的应用程序无法预测事件发生的顺序,事件循环会按照事件入队的顺序逐个处理它们。


感觉和handle,looper机制很像。
事件循环的运行方式与这段代码相似

while (eventQueue.waitForEvent()) { eventQueue.processNextEvent(); }

就是一直等待事件进入事件队列,总是取出最新的事件执行。

这个示例事件循环是同步的,且在单个线程上运行。
然而,大多数 Dart 应用程序需要同时处理不止一件事情。例如,客户端应用程序可能需要执行一个 HTTP 请求,同时还要监听用户点击按钮的操作。为了处理这种情况,Dart 提供了许多异步 API,如 Futures、Streams 和 async-await。这些 API 都是围绕事件循环构建的。

下面的例子中发起了一个网络请求

使用http库需要导包,可能会很慢,没有上网工具的话可以配置一下镜像地址。

当这段代码进入事件循环时,它会立即调用第一个子句 http.get,并返回一个 Future 对象。它还会告知事件循环,在 then () 子句中的回调函数要一直保留,直到 HTTP 请求完成解析。当请求解析完成后,事件循环就会执行该回调函数,并将请求的结果作为参数传入。


Dart 中的事件循环处理所有其他异步事件(例如 Stream 对象)时,通常采用的就是这种相同的模型。

Asynchronous programming

Futures

Future 代表一个异步操作的结果,该操作最终会以一个值或一个错误的形式完成。
在下面示例代码中,Future的返回类型表示一个承诺,最终会提供一个 String 值(或错误)。

Future<String> _readFileAsync(String filename) { final file = File(filename); // .readAsString() returns a Future. // .then() registers a callback to be executed when `readAsString` resolves. return file.readAsString().then((contents) { return contents.trim(); }); }

如果想要获取错误信息只需要和我一样,传递第二个参数

The async-await syntax

async 和 await 关键字提供了一种声明式的方式来定义异步函数并使用它们的结果。

下面是一个同步代码的示例,在等待文件 I/O 时会阻塞:

const String filename = 'C:\\Users\\Administrator\\Documents\\trae_projects\\dartStuday\\my_dart_project\\bin\\a.txt'; void main() { // Read some data. final fileData = _readFileSync(); final jsonData = jsonDecode(fileData); // Use that data. print('Number of JSON keys: ${jsonData.length}'); } String _readFileSync() { final file = File(filename); final contents = file.readAsStringSync(); return contents.trim(); }

下面是类似的代码,但做了一些修改(已突出显示)以使其成为异步代码:

main () 函数在_readFileAsync () 前使用 await 关键字,以便在原生代码(文件 I/O)执行时,让其他 Dart 代码(如事件处理程序)能够使用 CPU。使用 await 还有一个作用,就是将_readFileAsync () 返回的 Future<String>转换为 String。因此,contents 变量的隐式类型为 String。

await 关键字仅在函数体前带有 async 的函数中起作用。

如下图所示,在 readAsString () 执行非 Dart 代码(无论是在 Dart 运行时还是操作系统中)时,Dart 代码会暂停。一旦 readAsString () 返回一个值,Dart 代码的执行就会恢复。

Streams

Dart 还以流的形式支持异步代码。流会在未来提供值,并且会随着时间的推移反复提供。一个承诺会随着时间的推移提供一系列 int 值的对象,其类型为 Stream。

在下面的示例中,使用 Stream.periodic 创建的流每秒重复发送一个新的 int 值。

Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i); stream.listen((data) { print(data); });

下面是periodic函数的注释
/// 创建一个以指定[周期]为间隔、持续发送事件的流。
///
/// 事件值由调用[计算方法]生成。该回调函数的入参是一个整数,
/// 初始值为 0,每发送一个事件,该值自增 1。

await-for and yield

Await-for 是一种 for 循环,它会在提供新值时执行循环的每个后续迭代。换句话说,它用于 “遍历” 流。在这个示例中,当作为参数提供的流发出新值时,函数 sumStream 会发出一个新值。在返回值流的函数中,使用 yield 关键字而非 return 关键字。

Stream<int> sumStream(Stream<int> stream) async* { var sum = 0; await for (final value in stream) { yield sum += value; } }

下面是async*和async的区别

Isolates 隔离

除了异步 API 之外,Dart 还通过隔离区(isolates)支持并发。大多数现代设备都配备了多核 CPU。为了充分利用多核优势,开发者有时会使用并发运行的共享内存线程。然而,共享状态的并发容易出错,并且可能导致代码变得复杂。

与线程不同,所有 Dart 代码都在隔离区内部运行。借助隔离区,你的 Dart 代码可以同时执行多个独立任务,并且在有额外处理器内核可用时会加以利用。隔离区类似于线程或进程,但每个隔离区都有自己的内存和一个运行事件循环的单线程。

每个隔离区都有自己的全局字段,这确保了一个隔离区中的任何状态都无法从其他隔离区访问。隔离区之间只能通过消息传递进行通信。隔离区之间没有共享状态,这意味着 Dart 中不会出现像互斥锁、锁以及数据竞争这类并发复杂性问题。话虽如此,隔离区也不能完全防止竞态条件。

The main isolate 主隔离区

在大多数情况下,你根本无需考虑隔离区。Dart 程序默认在主隔离区中运行。
这是程序开始运行和执行的线程,如下图所示:

即便是单隔离程序也能顺畅运行。
在执行下一行代码之前,这些应用会使用异步等待(async-await)来等待异步操作完成。一个运行良好的应用启动迅速,能尽快进入事件循环。之后,该应用会及时响应每个排队的事件,并在必要时使用异步操作。

The isolate life cycle

如下图所示,每个隔离区都从运行一些 Dart 代码开始,例如 main () 函数。此 Dart 代码可能会注册一些事件监听器 —— 例如,用于响应用户输入或文件 I/O。当隔离区的初始函数返回时,如果需要处理事件,隔离区会继续存在。处理完事件后,隔离区便会退出。

Event handling

在客户端应用中,主隔离区的事件队列可能包含重绘请求以及点击和其他 UI 事件的通知。例如,下图展示了一个重绘事件,随后是一个点击事件,接着是两个重绘事件。事件循环按照先进先出的顺序从队列中获取事件。


事件处理在 main () 退出后发生在主隔离区。在下图中,main () 退出后,主隔离区处理第一个重绘事件。之后,主隔离区处理点击事件,随后是一个重绘事件。

如果同步操作占用过多处理时间,应用程序可能会变得无响应。在下图中,处理点击的代码耗时过长,因此后续事件的处理也会延迟。应用程序可能会出现冻结现象,其执行的任何动画也可能会卡顿。

在客户端应用中,过长的同步操作往往会导致卡顿(不流畅)的用户界面动画。更糟糕的是,用户界面可能会完全失去响应。

Background workers

如果你的应用程序的用户界面因耗时的计算(例如解析大型 JSON 文件)而变得无响应,可以考虑将该计算任务转移到工作隔离区(通常称为后台工作线程)。如下图所示,一种常见的情况是生成一个简单的工作隔离区,由它执行计算然后退出。工作隔离区在退出时会通过消息返回其结果。

工作器隔离区可以执行输入 / 输出操作(例如,读取和写入文件)、设置计时器等。它有自己的内存,并且不与主隔离区共享任何状态。工作器隔离区可以阻塞,而不会影响其他隔离区。

Using isolates

在 Dart 中使用隔离区有两种方式,具体取决于使用场景:

  • 使用 Isolate.run () 在单独的线程上执行单个计算。
  • 使用 Isolate.spawn () 创建一个隔离区,它将长期处理多条消息,或者作为后台工作程序。

在大多数情况下,Isolate.run 是推荐用于在后台运行进程的 API。

静态的 Isolate.run () 方法需要一个参数:一个将在新生成的隔离区上运行的回调函数。

int slowFib(int n) => n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2); // Compute without blocking current isolate. void fib40() async { print('Start fib40 ${DateTime.now()}'); var result = await Isolate.run(() => slowFib(40)); print('Fib(40) = $result ${DateTime.now()}'); }

下面是Isolate.spawn()函数的使用案例

import 'dart:isolate'; // 主隔离区 void main() async { print('主隔离区开始,ID: ${Isolate.current.hashCode}'); // 创建接收端口 final receivePort = ReceivePort(); // 创建新隔离区 final isolate = await Isolate.spawn( _isolateEntry, // 入口函数 receivePort.sendPort, // 初始消息 ); // 监听子隔离区消息 receivePort.listen((message) { print('收到子隔离区消息: $message'); if (message == 'done') { receivePort.close(); isolate.kill(); // 停止隔离区 } }); print('子隔离区已创建,ID: ${isolate.hashCode}'); } // 子隔离区入口函数(必须是顶层或静态函数) void _isolateEntry(SendPort sendPort) { print('子隔离区启动,ID: ${Isolate.current.hashCode}'); // 向主隔离区发送消息 sendPort.send('Hello from isolate!'); // 模拟工作 for (int i = 0; i < 3; i++) { sendPort.send('进度: $i'); } sendPort.send('done'); }

Performance and isolate groups

当一个隔离区调用 Isolate.spawn () 时,这两个隔离区拥有相同的可执行代码,并且处于同一个隔离区组中。隔离区组支持诸如代码共享等性能优化;新的隔离区会立即运行该隔离区组所拥有的代码。此外,只有当隔离区处于同一个隔离区组时,Isolate.exit () 才会生效。

在某些特殊情况下,你可能需要使用 Isolate.spawnUri (),它会利用指定 URI 处代码的副本来设置新的隔离区。不过,spawnUri () 比 spawn () 慢得多,而且新的隔离区不在其生成器的隔离组中。另一个性能影响是,当隔离区位于不同的组中时,消息传递会更慢。

Isolate.spawnUri ()主要是用在启动外部的代码副本,下面看案例

import 'dart:isolate'; import 'dart:io'; void main() async { final receivePort = ReceivePort(); // 创建指向外部 Dart 文件的 URI final uri = Uri.file('path/to/external_worker.dart'); // 使用 spawnUri 创建隔离区 final isolate = await Isolate.spawnUri( uri, // 外部文件 URI [], // 参数列表(传递给 main 函数) receivePort.sendPort, // 初始消息 ); receivePort.listen((message) { print('外部隔离区消息: $message'); }); await Future.delayed(Duration(seconds: 5)); isolate.kill(); // ✅ 可以停止,但没有exit优雅 print('隔离区已停止'); // 验证是否停止 print('隔离区是否存活?'); // 实际上没有直接的 isAlive 方法,但 kill 后资源会被回收 }
// external_worker.dart - 外部文件 import 'dart:isolate'; // 这是 spawnUri 的入口点 void main(List<String> args, SendPort sendPort) { // args 来自 spawnUri 的第二个参数 // sendPort 来自 spawnUri 的第三个参数 sendPort.send('来自外部文件的问候!'); sendPort.send('参数: $args'); }

Limitations of isolates隔离的局限性

隔离区不是线程。
如果你是从一种支持多线程的语言转而使用 Dart,那么期望隔离区的行为像线程一样是合情合理的,但事实并非如此。每个隔离区都有自己的状态,这确保了一个隔离区中的任何状态都无法被其他隔离区访问。因此,隔离区的能力受到它们对自身内存访问的限制。

例如,如果你有一个包含全局可变变量的应用程序,该变量在你生成的隔离区中会是一个独立的变量。如果你在生成的隔离区中修改了该变量,主隔离区中的该变量仍会保持不变。这就是隔离区的预期工作方式,在你考虑使用隔离区时,记住这一点很重要。

消息类型
通过 SendPort 发送的消息几乎可以是任何类型的 Dart 对象,但也有一些例外情况:

  • 绑定到特定隔离区的资源(Socket、ReceivePort)
  • 本地交互对象(Pointer、DynamicLibrary)
  • 内存管理对象(Finalizer)
  • 标记为不可发送的自定义类

设计启示:

  • 隔离区通信应该传递数据,而不是资源
  • 对于需要跨隔离区使用的资源,传递配置并在目标隔离区重新创建
  • 在设计跨隔离区API时,考虑对象的可序列化性

隔离区之间的同步阻塞通信

能够并行运行的隔离区数量是有限制的。不过,这个限制并不会影响 Dart 中隔离区之间通过消息进行的标准异步通信。你可以让数百个隔离区同时运行并推进工作。这些隔离区会以轮询的方式在 CPU 上进行调度,并且经常相互让出执行权。

在纯 Dart 之外,隔离区只能通过 FFI 调用 C 代码来进行同步通信。如果隔离区的数量超过限制,在 FFI 调用中通过同步阻塞来尝试隔离区之间的同步通信可能会导致死锁,除非采取特殊的处理措施。该限制并非硬编码为某个特定数字,而是根据 Dart 应用程序可用的 Dart 虚拟机堆大小计算得出的。

为避免这种情况,执行同步阻塞的 C 代码需要在执行阻塞操作前离开当前的隔离区,并在从 FFI 调用返回 Dart 之前重新进入该隔离区

隔离区在web

所有 Dart 应用都可以使用 async-await、Future 和 Stream 来进行非阻塞、交错的计算。不过,Dart Web 平台不支持隔离区。Dart Web 应用可以使用 Web Worker 在后台线程中运行脚本,这与隔离区类似。但 Web Worker 的功能和性能与隔离区存在一定差异。

例如,当 Web Worker 在线程之间发送数据时,它们会来回复制数据。不过,数据复制可能会非常缓慢,尤其是对于大型消息而言。隔离区(Isolates)也会做同样的事情,但还提供了一些 API,这些 API 能够更高效地传输存储消息的内存。

创建 Web Worker 和隔离区(Isolate)的方式也有所不同。你只能通过声明一个单独的程序入口点并对其进行单独编译来创建 Web Worker。启动 Web Worker 类似于使用 Isolate.spawnUri 来启动隔离区。你也可以使用 Isolate.spawn 来启动隔离区,这种方式需要的资源更少,因为它会复用一些与生成它的隔离区相同的代码和数据。而 Web Worker 没有与之等效的 API。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/1 3:04:04

希腊国家科学研究中心REGLUE:提升AI图像生成语义理解力

这项由希腊国家科学研究中心"Demokritos"的Giorgos Petsangourakis团队领导的研究发表于2025年12月&#xff0c;研究编号为arXiv:2512.16636v1。该研究还汇集了西阿提卡大学、捷克技术大学等多个机构的专家力量。有兴趣深入了解的读者可以通过arXiv数据库查询完整论文…

作者头像 李华
网站建设 2026/2/27 21:42:50

软件缺少msjint40.dll文件 下载修复方法

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

作者头像 李华
网站建设 2026/3/1 7:20:56

运维系列数据库系列【仅供参考】:达梦数据库:关键字和保留字

达梦数据库:关键字和保留字 关键字和保留字 摘要 正文 关键字和保留字 摘要 本文介绍了DM系统的关键字和系统保留字,保留字又分为SQL保留字等多种类型,可查询系统视图V$RESERVED WORDS了解详情。同时特别指出部分关键字不能作为表的列名。还罗列了从A到Z的大量关键字和保留…

作者头像 李华
网站建设 2026/2/28 22:37:55

百度自动驾驶出租车将于2026年进入伦敦市场

机器人出租车将于2026年进入伦敦市场。中国互联网巨头百度周一宣布&#xff0c;其Apollo Go自动驾驶网约车服务将在2026年上半年在英国首都进行试点运营&#xff0c;并得到Uber的支持。Uber表示"很兴奋能够加速英国在未来出行领域的领导地位&#xff0c;为伦敦人在明年带来…

作者头像 李华
网站建设 2026/2/28 21:28:56

JVM类加载过程:从字节码到运行时对象的诞生

字节码的"变身记"&#xff1a;从.class文件到运行时对象 一、类加载阶段 .class文件 -> 加载&#xff08;Loading) -> 链接(Linking) -> 初始化 -> 使用 -> 卸载 ^ 验证>准备>解析 前两篇我们完成了&#xff1a; 解码&#xff1a;拆解了.cla…

作者头像 李华
网站建设 2026/3/2 2:29:21

光储(VSG)并网系统:超级电容储能的魅力

光储&#xff08;虚拟同步发电机&#xff09;VSG并网系统&#xff0c;储能为超级电容。 波形好。在当今追求清洁能源高效利用的时代&#xff0c;光储&#xff08;虚拟同步发电机&#xff09;VSG并网系统逐渐成为研究和应用的热点。今天咱们就来唠唠这其中以超级电容作为储能装置…

作者头像 李华