Dart 中的并发
此页面包含 Dart 中并发编程工作原理的概念概述。它从高级层面解释了事件循环、异步语言特性和隔离区。有关在 Dart 中使用并发的更多实用代码示例,请阅读 异步支持 页面和 隔离区 页面。
Dart 中的并发编程指的是异步 API(如 Future
和 Stream
)和 隔离区 ,后者允许您将进程移动到单独的核心。
所有 Dart 代码都在隔离区中运行,从默认的主隔离区开始,并根据需要扩展到您显式创建的任何后续隔离区。当您生成一个新的隔离区时,它拥有自己独立的内存和自己的事件循环。事件循环使 Dart 中的异步和并发编程成为可能。
事件循环
#Dart 的运行时模型基于事件循环。事件循环负责执行程序的代码、收集和处理事件等等。
当您的应用程序运行时,所有事件都会添加到一个称为 事件队列 的队列中。事件可以是任何内容,从重绘 UI 的请求,到用户的点击和按键,再到磁盘的 I/O。因为您的应用程序无法预测事件发生的顺序,所以事件循环一次处理一个事件,按照它们排队的顺序处理事件。
事件循环的功能类似于以下代码:
while (eventQueue.waitForEvent()) {
eventQueue.processNextEvent();
}
此示例事件循环是同步的,并在单个线程上运行。但是,大多数 Dart 应用程序需要一次执行多项操作。例如,客户端应用程序可能需要执行 HTTP 请求,同时还要侦听用户点击按钮。为了处理这个问题,Dart 提供了许多异步 API,例如 Future、Stream 和 async-await 。这些 API 是围绕此事件循环构建的。
例如,考虑进行网络请求:
http.get('https://example.com').then((response) {
if (response.statusCode == 200) {
print('Success!');
}
}
当此代码到达事件循环时,它会立即调用第一个子句 http.get
并返回一个 Future
。它还会告诉事件循环保留 then()
子句中的回调,直到 HTTP 请求解析。发生这种情况时,它应该执行该回调,并将请求的结果作为参数传递。
此相同的模型通常是事件循环如何处理 Dart 中所有其他异步事件的方式,例如 Stream
对象。
异步编程
#本节总结了 Dart 中异步编程的不同类型和语法。如果您已经熟悉 Future
、 Stream
和 async-await,那么您可以跳到 隔离区部分 。
Futures
#Future
表示异步操作的结果,该操作最终将完成一个值或一个错误。
在此示例代码中, Future<String>
的返回类型表示最终提供 String
值(或错误)的承诺。
Future<String> _readFileAsync(String filename) {
final file = File(filename);
// .readAsString() 返回一个 Future。
// .then() 注册一个回调,当 `readAsString` 解析时执行。
return file.readAsString().then((contents) {
return contents.trim();
});
}
async-await 语法
#async
和 await
关键字提供了一种声明式的方法来定义异步函数并使用其结果。
这是一个同步代码示例,它在等待文件 I/O 时阻塞:
const String filename = 'with_keys.json';
void main() {
// 读取一些数据。
final fileData = _readFileSync();
final jsonData = jsonDecode(fileData);
// 使用这些数据。
print('JSON 密钥数量:${jsonData.length}');
}
String _readFileSync() {
final file = File(filename);
final contents = file.readAsStringSync();
return contents.trim();
}
这是一个类似的代码,但进行了更改(突出显示)以使其异步:
const String filename = 'with_keys.json';
void main() async {
// 读取一些数据。
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// 使用这些数据。
print('JSON 密钥数量:${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
main()
函数在 _readFileAsync()
前面使用 await
关键字,以允许其他 Dart 代码(例如事件处理程序)在原生代码(文件 I/O)执行时使用 CPU。使用 await
还会将 _readFileAsync()
返回的 Future<String>
转换为 String
。因此, contents
变量具有隐式类型 String
。
如下图所示,Dart 代码在 readAsString()
执行非 Dart 代码(在 Dart 运行时或操作系统中)时暂停。一旦 readAsString()
返回一个值,Dart 代码执行就会恢复。
Streams
#Dart 还支持流形式的异步代码。流在未来和重复地随时间提供值。承诺随着时间的推移提供一系列 int
值的类型为 Stream<int>
。
在下面的示例中,使用 Stream.periodic
创建的流每秒重复发出一个新的 int
值。
Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i);
await-for 和 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
、 await
、 Stream
和 Future
的更多信息,请查看 异步编程教程 。
隔离区
#除了 异步 API 之外,Dart 还通过隔离区支持并发。大多数现代设备都具有多核 CPU。为了利用多个核心,开发人员有时会使用并发运行的共享内存线程。但是,共享状态并发容易出错,并可能导致代码复杂。
所有 Dart 代码都在隔离区内运行,而不是线程。使用隔离区,您的 Dart 代码可以同时执行多个独立的任务,如果可用,则可以使用其他处理器核心。隔离区类似于线程或进程,但每个隔离区都有自己的内存和一个运行事件循环的单线程。
每个隔离区都有自己的全局字段,确保任何隔离区中的状态都无法从任何其他隔离区访问。隔离区只能通过消息传递相互通信。隔离区之间没有共享状态意味着不会出现像 互斥锁或锁和 数据竞争 这样的并发复杂性。也就是说,隔离区并不能完全防止竞争条件。有关此并发模型的更多信息,请阅读有关 Actor 模型 的内容。
主隔离区
#在大多数情况下,您根本不需要考虑隔离区。Dart 程序默认在主隔离区中运行。这是程序开始运行和执行的线程,如下图所示:
即使是单隔离区程序也可以顺利执行。在继续执行下一行代码之前,这些应用程序使用 async-await 等待异步操作完成。一个行为良好的应用程序启动速度很快,尽快进入事件循环。然后,应用程序会及时响应每个排队的事件,根据需要使用异步操作。
隔离区生命周期
#如下图所示,每个隔离区都通过运行一些 Dart 代码(例如 main()
函数)来启动。此 Dart 代码可能会注册一些事件监听器——例如,响应用户输入或文件 I/O。当隔离区的初始函数返回时,如果需要处理事件,隔离区将保留下来。处理完事件后,隔离区将退出。
事件处理
#在客户端应用程序中,主隔离区的事件队列可能包含重绘请求和点击和其他 UI 事件的通知。例如,下图显示了一个重绘事件,然后是一个点击事件,然后是两个重绘事件。事件循环按照先进先出的顺序从队列中获取事件。
main()
退出后,事件处理发生在主隔离区。在下图中, main()
退出后,主隔离区处理第一个重绘事件。之后,主隔离区处理点击事件,然后是重绘事件。
如果同步操作花费了过多的处理时间,应用程序可能会变得无响应。在下图中,点击处理代码花费的时间过长,因此后续事件处理得太迟。应用程序可能会出现冻结现象,并且它执行的任何动画都可能不流畅。
在客户端应用程序中,同步操作时间过长的结果通常是 不流畅(非平滑)的 UI 动画 。更糟糕的是,UI 可能会完全无响应。
后台工作进程
#如果您的应用程序的 UI 由于耗时的计算(例如 解析大型 JSON 文件 )而变得无响应,请考虑将该计算卸载到工作隔离区(通常称为 后台工作进程 )。 一个常见的案例,如下图所示,是生成一个简单的执行计算然后退出的工作隔离区。工作隔离区在退出时通过消息返回其结果。
工作隔离区可以执行 I/O(例如读取和写入文件)、设置计时器等等。它拥有自己的内存,并且不与主隔离区共享任何状态。工作隔离区可以阻塞而不会影响其他隔离区。
使用隔离区
#根据用例,有两种方法可以在 Dart 中使用隔离区:
- 使用
Isolate.run()
在单独的线程上执行单个计算。 - 使用
Isolate.spawn()
创建一个隔离区,该隔离区将随时间处理多个消息,或一个后台工作进程。有关使用长期运行隔离区的更多信息,请阅读 隔离区 页面。
在大多数情况下, Isolate.run
是在后台运行进程的推荐 API。
Isolate.run()
#静态 Isolate.run()
方法需要一个参数:一个将在新生成的隔离区上运行的回调。
int slowFib(int n) => n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
// 计算时不阻塞当前隔离区。
void fib40() async {
var result = await Isolate.run(() => slowFib(40));
print('Fib(40) = $result');
}
性能和隔离区组
#当隔离区调用 Isolate.spawn()
时,这两个隔离区具有相同的可执行代码,并且位于同一个 隔离区组 中。隔离区组可以启用性能优化,例如代码共享;新的隔离区会立即运行隔离区组拥有的代码。此外, Isolate.exit()
仅在隔离区位于同一隔离区组中时才有效。
在某些特殊情况下,您可能需要使用 Isolate.spawnUri()
,它使用指定 URI 处的代码副本设置新的隔离区。但是, spawnUri()
比 spawn()
慢得多,并且新的隔离区不在其生成者的隔离区组中。另一个性能影响是,当隔离区位于不同的组中时,消息传递速度会变慢。
隔离区的限制
#隔离区不是线程
#如果您是从具有多线程的语言转向 Dart,那么期望隔离区像线程一样运行是合理的,但事实并非如此。每个隔离区都有自己的状态,确保任何隔离区中的状态都无法从任何其他隔离区访问。因此,隔离区受到其访问自己内存的限制。
例如,如果您有一个带有全局可变变量的应用程序,则该变量将在您的生成的隔离区中成为一个单独的变量。如果您在生成的隔离区中更改该变量,则它将在主隔离区中保持不变。这就是隔离区的工作方式,在考虑使用隔离区时,务必记住这一点。
消息类型
#通过 SendPort
发送的消息可以是几乎任何类型的 Dart 对象,但有一些例外:
- 具有本机资源的对象,例如
Socket
。 ReceivePort
DynamicLibrary
Finalizable
Finalizer
NativeFinalizer
Pointer
UserTag
- 使用
@pragma('vm:isolate-unsendable')
标记的类的实例
除了这些例外情况外,任何对象都可以发送。查看 SendPort.send
文档以了解更多信息。
请注意, Isolate.spawn()
和 Isolate.exit()
对 SendPort
对象进行了抽象,因此它们也受到相同的限制。
隔离区之间同步阻塞通信
#可以并行运行的隔离区数量是有限制的。此限制不影响 Dart 中通过消息进行的隔离区之间的标准 异步 通信。您可以同时运行数百个隔离区并取得进展。隔离区以循环方式调度到 CPU 上,并且经常互相让步。
隔离区只能在纯 Dart 之外进行 同步 通信,使用FFI 通过 C 代码来实现。如果隔离区的数量超过限制,则尝试通过 FFI 调用中的同步阻塞来进行隔离区之间的同步通信可能会导致死锁,除非采取特殊措施。此限制并非硬编码为特定数字,而是根据 Dart 应用程序可用的 Dart VM 堆大小来计算的。
为了避免这种情况,执行同步阻塞的 C 代码需要在执行阻塞操作之前离开当前隔离区,并在从 FFI 调用返回到 Dart 之前重新进入它。阅读Dart_EnterIsolate
和Dart_ExitIsolate
以了解更多信息。
Web 上的并发
#所有 Dart 应用程序都可以使用 async-await
、 Future
和 Stream
进行非阻塞、交错的计算。但是, Dart Web 平台 不支持隔离区。Dart Web 应用程序可以使用 Web 工作线程 在后台线程中运行脚本,类似于隔离区。但是,Web 工作线程的功能和能力与隔离区略有不同。
例如,当 Web 工作线程在线程之间发送数据时,它们会来回复制数据。但是,数据复制可能会非常慢,特别是对于大型消息而言。隔离区也是如此,但它还提供可以更有效地 传输 保存消息的内存的 API。
创建 Web 工作线程和隔离区也有所不同。您只能通过声明单独的程序入口点并单独编译它来创建 Web 工作线程。启动 Web 工作线程类似于使用 Isolate.spawnUri
启动隔离区。您还可以使用 Isolate.spawn
启动隔离区,这需要更少的资源,因为它 重用了一些与生成隔离区相同的代码和数据 。Web 工作线程没有等效的 API。
附加资源
#- 如果你正在使用许多隔离区,请考虑在 Flutter 中使用
IsolateNameServer
,或package:isolate_name_server
,它为非 Flutter Dart 应用程序提供类似的功能。 - 阅读有关 Actor 模型 的更多信息,Dart 的隔离区就是基于此模型的。
- 有关
Isolate
API 的其他文档:
除非另有说明,否则本网站上的文档反映的是 Dart 3.6.0。页面最后更新于 2025-02-05。 查看源代码 或 报告问题.