本页包含 Dart 中并发编程如何工作的概念概述。它解释了事件循环、异步语言功能,并与高层隔离。有关在 Dart 中使用并发的更多实用代码示例,请阅读异步支持页面和隔离页面。
Dart 中的并发编程指的是异步 API,例如 Future 和 Stream 以及隔离,它允许您将进程移动到单独的核心。
所有 Dart 代码都在隔离区中运行,从默认的主隔离区开始,并可选择扩展到您显式创建的任何后续隔离区。当您生成一个新的隔离时,它有自己的隔离内存和自己的事件循环。事件循环使得 Dart 中的异步和并发编程成为可能。
事件循环
Dart 的运行时模型基于事件循环。事件循环负责执行程序的代码、收集和处理事件等等。
当您的应用程序运行时,所有事件都会添加到一个队列中,称为事件队列。事件可以是任何内容,从重新绘制 UI 的请求到用户的点击和击键,再到磁盘的 I/O。由于您的应用程序无法预测事件将发生的顺序,因此事件循环会按照事件的排队顺序一次处理一个事件。
事件循环的运行方式类似于以下代码:
while (eventQueue.waitForEvent()) {
eventQueue.processNextEvent();
}
此示例事件循环是同步的并且在单个线程上运行。然而,大多数 Dart 应用程序一次需要做不止一件事。例如,客户端应用程序可能需要执行 HTTP 请求,同时还侦听用户点击按钮。为了解决这个问题,Dart 提供了许多异步 API,例如 Futures、Streams 和 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,那么您可以跳到隔离部分。
期货
Future 表示异步操作的结果,该操作最终将完成并带有一个值或一个错误。
在此示例代码中, Future
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();
});
}
异步等待语法
async 和 await 关键字提供了一种声明性方式来定义异步函数并使用其结果。
下面是一些在等待文件 I/O 时发生阻塞的同步代码的示例:
const String filename = 'with_keys.json';
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();
}
这是类似的代码,但进行了更改(突出显示)以使其异步:
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
ain() 函数在 _readFileAsync() 前面使用 await 关键字,让其他 Dart 代码(例如事件处理程序)使用 CPU,而本机代码(文件 I/ O) 执行。使用 await 还具有将 _readFileAsync() 返回的 Future
await 关键字仅适用于函数体之前有 async 的函数。
如下图所示,在 Dart 运行时或操作系统中,当 readAsString() 执行非 Dart 代码时,Dart 代码会暂停。一旦 readAsString() 返回值,Dart 代码就会恢复执行。
流
Dart 还支持流形式的异步代码。流在未来提供价值,并且随着时间的推移不断重复。随着时间的推移提供一系列 int 值的承诺的类型为 Stream
在以下示例中,使用 Stream.periodic 创建的流每秒重复发出一个新的 int 值。
Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i);
等待和产量
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 的更多信息,请访问异步编程 Codelab。
分离物
除了异步 API 之外,Dart 还通过隔离支持并发。大多数现代设备都具有多核 CPU。为了利用多核的优势,开发人员有时会使用并发运行的共享内存线程。然而,共享状态并发很容易出错,并且可能导致代码复杂。
所有 Dart 代码都在隔离区内运行,而不是线程。使用隔离,您的 Dart 代码可以同时执行多个独立任务。隔离就像线程或进程,但每个隔离都有自己的内存和运行事件循环的单个线程。
每个隔离区都有自己的全局字段,确保隔离区中的任何状态都无法从任何其他隔离区访问。隔离体只能通过消息传递来相互通信。隔离之间没有共享状态意味着 Dart 中不会出现互斥锁或锁以及数据竞争等并发复杂性。也就是说,隔离并不能完全防止竞争条件。有关此并发模型的更多信息,请阅读 Actor 模型。
使用隔离,您的 Dart 代码可以同时执行多个独立任务,并使用额外的处理器核心(如果可用)。隔离就像线程或进程,但每个隔离都有自己的内存和运行事件循环的单个线程。
只有 Dart Native 平台实现了isolate。要了解有关 Dart Web 平台的更多信息,请参阅 Web 上的并发部分。
主要分离物
在大多数情况下,您根本不需要考虑分离株。 Dart 程序默认在主隔离区中运行。是程序开始运行并执行的线程,如下图所示:
即使是单隔离程序也可以顺利执行。在继续执行下一行代码之前,这些应用程序使用 async-await 来等待异步操作完成。行为良好的应用程序启动速度很快,尽快进入事件循环。然后,应用程序立即响应每个排队的事件,并根据需要使用异步操作。
隔离生命周期
如下图所示,每个isolate都是从运行一些Dart代码开始的,例如 main() 函数。此 Dart 代码可能会注册一些事件侦听器,例如响应用户输入或文件 I/O。当isolate的初始函数返回时,如果它需要处理事件,isolate就会留下来。处理完事件后,isolate 退出。
事件处理
在客户端应用程序中,主隔离的事件队列可能包含重绘请求以及点击和其他 UI 事件的通知。例如,下图显示了一个重绘事件,后跟一个点击事件,然后是两个重绘事件。事件循环按照先进先出的顺序从队列中获取事件。
main() 退出后,事件处理发生在主隔离上。下图中, main() 退出后,主isolate处理第一个重绘事件。之后,主隔离处理点击事件,然后是重绘事件。
如果同步操作花费太多处理时间,应用程序可能会变得无响应。在下图中,点击处理代码花费的时间太长,因此后续事件处理得太晚。应用程序可能会冻结,并且它执行的任何动画都可能不稳定。
在客户端应用程序中,太长的同步操作通常会导致 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);
// Compute without blocking current isolate.
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 对象进行抽象,因此它们受到相同的限制。
Web 上的并发
所有 Dart 应用程序都可以使用 async-await 、 Future 和 Stream 进行非阻塞、交错计算。然而,Dart Web 平台不支持隔离。 Dart Web 应用程序可以使用 Web Worker 在后台线程中运行脚本,类似于隔离。不过,网络工作者的功能和能力与隔离人员有些不同。
例如,当网络工作线程在线程之间发送数据时,它们会来回复制数据。但是,数据复制可能非常慢,尤其是对于大型消息。隔离区执行相同的操作,但还提供 API,可以更有效地传输保存消息的内存。
创建网络工作者和隔离者也有所不同。您只能通过声明单独的程序入口点并单独编译来创建 Web Worker。启动 Web Worker 类似于使用 Isolate.spawnUri 启动隔离。您还可以使用 Isolate.spawn 启动隔离,这需要更少的资源,因为它重用了一些与生成隔离相同的代码和数据。 Web Worker 没有等效的 API。
其他资源
- 如果您使用许多隔离,请考虑 Flutter 中的 IsolateNameServer 或为非 Flutter Dart 应用程序提供类似功能的 package:isolate_name_server 。
- 阅读有关 Actor 模型的更多信息,Dart 的隔离所基于该模型。
- 有关 Isolate API 的其他文档:
- Isolate.exit()
- Isolate.spawn()
- ReceivePort
- SendPort