Asynchronous programming
异步编程
Dart 库充满了返回 Future 或 Stream 对象的函数。这些函数是异步的:它们在设置可能耗时的作(如 I/O)后返回,而无需等待该作完成。
async
和 await
关键字支持异步编程,允许您编写看起来类似于同步代码的异步代码。
Handling Futures 处理期货
当您需要已完成的 Future 的结果时,您有两个选项:
- 使用
async
和await
,如此处和 异步编程教程 。 - 使用 Future API,如 dart:async 文档 。
使用 async
和 await
的代码是异步的,但它看起来与同步代码非常相似。例如,下面是一些使用 await
的代码 要等待异步函数的结果,请执行以下作:
await lookUpVersion();
要使用 await
,代码必须位于 async
函数中,该函数标记为 async
:
Future<void> checkVersion() async {
var version = await lookUpVersion();
// Do something with version
}
信息提示
尽管异步
函数可能会执行耗时的作,但它不会等待这些作。相反,async
函数仅在遇到其第一个 await
表达式之前执行。然后,它返回一个 Future
对象,仅在 await
表达式完成后才恢复执行。
使用 try
、catch
和 finally
处理使用 await
的代码中的错误和清理:
try {
version = await lookUpVersion();
} catch (e) {
// React to inability to look up the version
}
您可以在异步
函数中多次使用 await
。例如,以下代码等待 3 次函数的结果:
var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
在 await 表达式
中,expression
的值通常是 Future;如果不是,则该值会自动包装在 Future 中。此 Future 对象表示返回对象的 Promise。await 表达式
的值是返回的对象。await 表达式使执行暂停,直到该对象可用。
如果在使用 await
时收到编译时错误,请确保 await
位于异步
函数中。 例如,要在应用的 main()
函数中使用 await
, 必须将 main()
的主体标记为 async
:
void main() async {
checkVersion();
print('In main: version is ${await lookUpVersion()}');
}
信息提示
前面的示例使用异步
函数 (checkVersion()
) 而不等待结果,如果代码假定函数已完成执行,则这种做法可能会导致问题。要避免此问题,请使用 unawaited_futures Linter 规则 。
Declaring async functions
声明异步函数
async
函数是其主体标有 async
修饰符的函数。
将 async
关键字添加到函数中会使其返回 Future。例如,请考虑以下同步函数,它返回一个 String:
String lookUpVersion() => '1.0.0';
如果将其更改为异步函数(
例如,因为将来的实现将非常耗时),则返回的值是 Future:
Future<String> lookUpVersion() async => '1.0.0';
请注意,函数的主体不需要使用 Future API。如有必要,Dart 会创建 Future 对象。如果您的函数未返回有用的值,请将其返回类型 Future<void>
。
Handling Streams 处理流
当您需要从 Stream 中获取值时,您有两个选项:
- 使用
async
和异步 for 循环 (await for
)。 - 使用 Stream API,如 dart:async 文档 。
信息提示
在使用 await for
之前,请确保它使代码更清晰,并且您确实希望等待 stream 的所有结果。例如,您通常不应将 await 用于
UI 事件侦听器,因为 UI 框架会发送无穷无尽的事件流。
异步 for 循环具有以下形式:
await for (varOrType identifier in expression) {
// Executes each time the stream emits a value.
}
expression
的值必须具有 Stream 类型。执行过程如下:
- 等待流发出值。
- 执行 for 循环的主体,并将变量设置为该发出的值。
- 重复 1 和 2,直到流关闭。
要停止侦听流,您可以使用 break
或 return
语句,该语句跳出 for 循环并取消订阅流。
如果在实现异步 for 循环时遇到编译时错误,请确保 await for
位于异步
函数中。 例如,要在应用的 main()
函数中使用异步 for 循环, 必须将 main()
的主体标记为 async
:
void main() async {
// ...
await for (final request in requestServer) {
handleRequest(request);
}
// ...
}
Isolates 分离
每当应用程序处理足够大以暂时阻止其他计算的计算时,都应该使用 isolates。最常见的例子是在 Flutter 应用程序中,当你需要执行大型计算时,这可能会导致 UI 变得无响应。
对于何时必须使用隔离对象,没有任何规定,但以下是它们可能有用的更多情况:
对于何时必须使用隔离对象,没有任何规定,但以下是它们可能有用的更多情况:
- 解析和解码超大型 JSON blob。
- 处理和压缩照片、音频和视频。
- 转换音频和视频文件。
- 对大型列表或文件系统执行复杂的搜索和筛选。
- 执行 I/O,例如与数据库通信。
- 处理大量网络请求。
Implementing a simple worker isolate
实现一个简单的 worker isolate
这些示例实现了一个 main isolate 这会生成一个简单的 worker isolate。 Isolate.run() 简化了设置和管理 worker isolate 的步骤:
- Spawns (starts and creates) an isolate.
生成 (启动和创建) 隔离。 - Runs a function on the spawned isolate.
在生成的 isolate 上运行函数。 - Captures the result. 捕获结果。
- Returns the result to the main isolate.
将结果返回到主 isolate。 - Terminates the isolate once work is complete.
工作完成后终止 isolate。 - Checks, captures, and throws exceptions and errors back to the main isolate.
检查、捕获异常和错误并将其引发回主 isolate。
提示
如果你使用的是 Flutter,则可以使用 Flutter 的计算函数 而不是 Isolate.run()。
Running an existing method in a new isolate
在新的 isolate 中运行现有方法
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final jsonData = await Isolate.run(_readAndParseJson);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
- 将您希望它作为第一个参数执行的函数传递给 worker: isolate。在此示例中,它是现有函数
_readAndParseJson():
Future<Map<String, dynamic>> _readAndParseJson() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
}
3、Isolate.run()
获取 _readAndParseJson()
返回的结果,并将值发送回主 isolate,关闭 worker isolate。
4、worker isolate 将保存结果的内存传输到 main isolate。它不会复制数据。worker isolate 执行验证传递,以确保允许传输对象。
_readAndParseJson()
是一个现有的异步函数,可以很容易地直接在主 isolate 中运行。使用 Isolate.run()
来运行它会启用并发。worker isolate 完全抽象了 _readAndParseJson()
的计算。它可以在不阻塞主隔离器的情况下完成。
Isolate.run()
的结果始终是 Future,因为主 isolate 中的代码继续运行。worker isolate 执行的计算是同步的还是异步的都不会影响主 isolate,因为它以任何一种方式并发运行。
Sending closures with isolates
发送带有 isolate 的闭包
你也可以直接在主 isolate 中使用函数字面量或 closure 通过 run()
创建一个简单的 worker isolate。
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final jsonData = await Isolate.run(() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
});
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
此示例完成与上一个相同的作。一个新的 isolate 生成,计算一些内容,并将结果发回。
但是,现在 isolate 会发送一个闭包 。与典型的命名函数相比,闭包在功能方式和写入代码的方式方面受到的限制较小。在此示例中,Isolate.run()
并发执行类似于本地代码的内容。从这个意义上说,你可以想象 run()
就像 “run in parallel” 的控制流运算符一样工作。
Sending multiple messages between isolates with ports
在具有端口的 isolate 之间发送多条消息
短期隔离使用方便,但需要性能开销才能生成新的 Isolate 并将对象从一个 Isolate 复制到另一个 Isolate。如果您的代码依赖于使用 Isolate.run
重复运行相同的计算,则可以通过创建不会立即退出的长寿命 isolate 来提高性能。
为此,您可以使用一些低级隔离 API,这些 API Isolate.run
摘要:
- Isolate.spawn() and Isolate.exit()
Isolate.spawn() 和 Isolate.exit() - ReceivePort and SendPort
ReceivePort 和 SendPort - SendPort.send() methodSendPort.send() 方法
本节介绍在新生成的 isolate 和主 isolate 之间建立 2 路通信所需的步骤。第一个示例 Basic ports 在较高级别介绍了该过程。第二个示例(Robust ports)逐渐向第一个示例添加了更实用、更真实的功能。
ReceivePort
and SendPort
ReceivePort
和 SendPort
在 isolate 之间设置长期通信需要两个类(除了 Isolate
):ReceivePort
和 SendPort
。这些端口是 isolate 相互通信的唯一方式。
ReceivePort
是一个对象,用于处理从其他 isolate 发送的消息。这些消息通过 SendPort
发送。
信息提示
一个 SendPort
对象只与一个 ReceivePort
相关联,但单个 ReceivePort
可以有多个 SendPort
。创建 ReceivePort
时,它会为自己创建一个 SendPort
。您可以创建其他 SendPort
,以便将消息发送到现有 ReceivePort
。
端口的行为类似于 Stream 对象(实际上,接收端口实现了 Stream
!您可以将 SendPort
和 ReceivePort
分别视为 Stream 的 StreamController
和侦听器。SendPort
类似于 StreamController
,因为您使用 SendPort.send() 方法向它们“添加”消息,并且这些消息由侦听器处理,在本例中为 ReceivePort
。然后,ReceivePort
通过将消息作为参数传递给您提供的回调来处理它收到的消息。
Setting up ports 设置端口
新生成的 isolate 仅包含它通过 Isolate.spawn
调用。如果需要主 isolate 在初始创建后继续与生成的 isolate 通信,则必须设置一个通信通道,使生成的 isolate 可以向主 isolate 发送消息。Isolate 只能通过消息传递进行通信。他们无法“看到”彼此的记忆,这就是“隔离”这个名字的由来。
要设置此 2 路通信,请首先创建一个 ReceivePort ,然后在使用 Isolate.spawn
生成新 isolate 时将其 SendPort 作为参数传递给新 isolate。然后,新的 isolate 创建自己的 ReceivePort
,并发送其 SendPort
返回到 SendPort
上,它由主 isolate 传递。主 isolate 接收此 SendPort
,现在双方都有一个开放的通道来发送和接收消息。
- 在主隔离中创建一个
ReceivePort
。SendPort
将自动创建为ReceivePort
上的属性。 - 使用
Isolate.spawn()
生成 worker isolate - 将对
ReceivePort.sendPort
的引用作为第一条消息传递给 worker isolate。 - 在 worker isolate 中创建另一个新的
ReceivePort
。 - 将对 worker isolate 的
ReceivePort.sendPort
的引用作为第一条消息传回主 isolate。
除了创建端口和设置通信外,您还需要告诉端口在收到消息时该怎么做。这是使用每个相应 ReceivePort
上的 listen
方法完成的。
- 通过主 isolate 对 worker isolate 的
SendPort
的 - 通过 worker isolate 的
ReceivePort 的 ReceivePort
中。这是要移出主 isolate 的计算的执行位置。 - 通过 worker isolate 对主 isolate 的
SendPort
的引用发送返回消息。 - 通过主 isolate 的
ReceivePort
上的侦听器接收消息。
Basic ports example 基本端口示例
此示例演示了如何设置一个长期工作线程隔离,并在它与主隔离之间进行双向通信。该代码使用将 JSON 文本发送到新 isolate 的示例,在 JSON 发送回主 isolate 之前,将在其中解析和解码 JSON。
警告请注意
此示例旨在教授生成可以随时间推移发送和接收多条消息的新 isolate 所需的最低限度 。
它不涵盖生产软件中预期的重要功能,例如错误处理、关闭端口和消息排序。
Step 1: Define the worker class
第 1 步:定义 worker 类
首先,为你的后台 worker isolate 创建一个类。此类包含您需要的所有功能:
- Spawn an isolate. 生成一个隔离。
- Send messages to that isolate.
向该隔离地址发送消息。 - Have the isolate decode some JSON.
让 isolate 解码一些 JSON。 - Send the decoded JSON back to the main isolate.
将解码后的 JSON 发送回主隔离。
该类公开了两个公共方法:一个用于生成 worker isolate,另一个用于处理向该 worker isolate 发送消息。
此示例中的其余部分将逐个介绍如何填写类方法。
class Worker {
Future<void> spawn() async {
// TODO: Add functionality to spawn a worker isolate.
}
void _handleResponsesFromIsolate(dynamic message) {
// TODO: Handle messages sent back from the worker isolate.
}
static void _startRemoteIsolate(SendPort port) {
// TODO: Define code that should be executed on the worker isolate.
}
Future<void> parseJson(String message) async {
// TODO: Define a public method that can
// be used to send messages to the worker isolate.
}
}
Step 2: Spawn a worker isolate
第 2 步:生成一个 worker isolate
在 Worker.spawn
方法中,您将对用于创建 worker isolate 并确保它可以接收和发送消息的代码进行分组。
- 首先,创建一个
ReceivePort
。这允许主 isolate 接收从新生成的 worker isolate 发送的消息。 - Next, add a listener to the receive port to handle messages the worker isolate will send back. The callback passed to the listener,
_handleResponsesFromIsolate
, will be covered in step 4.
接下来,向接收端口添加侦听器,以处理 worker isolate 将发回的消息。传递给侦听器_handleResponsesFromIsolate
的回调将在步骤 4 中介绍。 - Finally, spawn the worker isolate with
Isolate.spawn
. It expects two arguments: a function to be executed on the worker isolate (covered in step 3), and thesendPort
property of the receive port.
最后,使用Isolate.spawn
生成 worker isolate。它需要两个参数:要在 worker isolate 上执行的函数(在步骤 3 中介绍)和接收端口的sendPort
属性。
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}
在 worker isolate 上调用 receivePort.sendPort
参数时,该参数将作为参数传递给回调 (_startRemoteIsolate
)。这是确保 worker isolate 有办法将消息发送回主 isolate 的第一步。
Step 3: Execute code on the worker isolate
第 3 步:在 worker isolate 上执行代码
在此步骤中,您将定义发送到工作程序 isolate 的方法 _startRemoteIsolate
在生成时执行。此方法类似于 worker isolate 的 “main” 方法。
- 首先,创建另一个新的
ReceivePort
。此端口接收来自主 isolate 的未来消息。 - 接下来,将该端口的
SendPort
发送回主 isolate。 - 最后,向新的
ReceivePort
添加侦听器。此侦听器处理主 isolate 发送到工作器 isolate 的消息。
static void _startRemoteIsolate(SendPort port) {
final receivePort = ReceivePort();
port.send(receivePort.sendPort);
receivePort.listen((dynamic message) async {
if (message is String) {
final transformed = jsonDecode(message);
port.send(transformed);
}
});
}
worker 的 ReceivePort
上的侦听器对从主 isolate 传递的 JSON 进行解码,然后将解码后的 JSON 发送回主 isolate。
此侦听器是从主 isolate 发送到 worker isolate 的消息的入口点。 这是您告诉 worker isolate 将来要执行哪些代码的唯一机会。
Step 4: Handle messages on the main isolate
步骤 4:处理主隔离岛上的消息
最后,你需要告诉主 isolate 如何处理从 worker isolate 发送回主 isolate 的消息。为此,您需要填写 _handleResponsesFromIsolate
方法。回想一下,此方法将传递给 receivePort.listen
方法,如步骤 2 中所述:
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}
另请记住,您在步骤 3 中将 SendPort
发送回主 isolate。此方法处理该 SendPort
的接收,以及处理将来的消息(将解码为 JSON)。
- 首先,检查消息是否为
SendPort
。如果是这样,请将该端口分配给类的_sendPort
属性,以便以后可以使用它来发送消息。 - 接下来,检查消息的类型是否为
Map<String、dynamic>
,即预期的解码 JSON 类型。如果是这样,请使用特定于应用程序的逻辑处理该消息。在此示例中,将打印消息。
void _handleResponsesFromIsolate(dynamic message) {
if (message is SendPort) {
_sendPort = message;
_isolateReady.complete();
} else if (message is Map<String, dynamic>) {
print(message);
}
}
Step 5: Add a completer to ensure your isolate is set-up
第 5 步:添加完成器以确保您的分离株已设置
要完成该类,请定义一个名为 parseJson
的公共方法,该方法负责将消息发送到 worker isolate。它还需要确保在完全设置 isolate 之前可以发送消息。要处理此问题,请使用 Completer。
- 首先,添加一个名为
Completer
的类级属性并将其命名为_isolateReady
。 - 接下来,如果消息是
SendPort
,则在_handleResponsesFromIsolate
方法(在步骤 4 中创建)中对完成程序添加对complete()
的调用。 - 最后,在
parseJson
方法中,在添加_sendPort.send
之前添加await _isolateReady.future
。这确保了在生成工作程序 isolate 并将其SendPort
发送回主隔离之前,无法将任何消息发送到工作程序 isolate。
Future<void> parseJson(String message) async {
await _isolateReady.future;
_sendPort.send(message);
}
Complete example 完整示例
#展开以查看完整示例
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
void main() async {
final worker = Worker();
await worker.spawn();
await worker.parseJson('{"key":"value"}');
}
class Worker {
late SendPort _sendPort;
final Completer<void> _isolateReady = Completer.sync();
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}
void _handleResponsesFromIsolate(dynamic message) {
if (message is SendPort) {
_sendPort = message;
_isolateReady.complete();
} else if (message is Map<String, dynamic>) {
print(message);
}
}
static void _startRemoteIsolate(SendPort port) {
final receivePort = ReceivePort();
port.send(receivePort.sendPort);
receivePort.listen((dynamic message) async {
if (message is String) {
final transformed = jsonDecode(message);
port.send(transformed);
}
});
}
Future<void> parseJson(String message) async {
await _isolateReady.future;
_sendPort.send(message);
}
}
Robust ports example 稳健的端口示例
前面的示例介绍了设置具有双向通信的长期隔离所需的基本构建块。如前所述,该示例缺少一些重要的功能,例如错误处理、在不再使用端口时关闭端口的能力,以及在某些情况下消息排序的不一致。
此示例通过创建一个具有这些附加功能以及更多功能的长期工作程序隔离来扩展第一个示例中的信息,并遵循更好的设计模式。尽管此代码与第一个示例有相似之处,但它不是该示例的扩展。
Step 1: Define the worker class
第 1 步:定义 worker 类
#
首先,为你的后台 worker isolate 创建一个类。此类包含您需要的所有功能:
- Spawn an isolate. 生成一个隔离。
- Send messages to that isolate.
向该隔离地址发送消息。 - Have the isolate decode some JSON.
让 isolate 解码一些 JSON。 - Send the decoded JSON back to the main isolate.
将解码后的 JSON 发送回主隔离。
该类公开了三个公共方法:一个用于创建 worker isolate,一个用于处理向该 worker isolate 发送消息,以及一个可以在不再使用端口时关闭端口。
class Worker {
final SendPort _commands;
final ReceivePort _responses;
Future<Object?> parseJson(String message) async {
// TODO: Ensure the port is still open.
_commands.send(message);
}
static Future<Worker> spawn() async {
// TODO: Add functionality to create a new Worker object with a
// connection to a spawned isolate.
throw UnimplementedError();
}
Worker._(this._responses, this._commands) {
// TODO: Initialize main isolate receive port listener.
}
void _handleResponsesFromIsolate(dynamic message) {
// TODO: Handle messages sent back from the worker isolate.
}
static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async {
// TODO: Handle messages sent back from the worker isolate.
}
static void _startRemoteIsolate(SendPort sp) {
// TODO: Initialize worker isolate's ports.
}
}
信息提示
在此示例中,SendPort
和 ReceivePort
实例遵循最佳实践命名约定,其中它们根据主 isolate 进行命名。通过 SendPort
从主 isolate 发送到 worker isolate 的消息称为命令 ,发送回主 isolate 的消息称为响应 。
Step 2: Create a RawReceivePort
in the Worker.spawn
method
第 2 步:在 Worker.spawn
方法中创建 RawReceivePort
在生成 isolate 之前,您需要创建一个 RawReceivePort,这是一个较低级别的 ReceivePort
。使用 RawReceivePort
是一种首选模式,因为它允许您将 isolate 启动逻辑与处理 isolate 上消息传递的 logic 分开。
在 Worker.spawn
方法中:
- 首先,创建
RawReceivePort
。此ReceivePort
仅负责接收来自 worker isolate 的初始消息,该 isolate 将是SendPort
。 - 接下来,创建一个
Completer
,它将指示 isolate 何时准备好接收消息。完成后,它将返回一条带有ReceivePort
和SendPort
的记录。 - 接下来,定义
RawReceivePort.handler
属性。此属性是一个Function?
,其行为类似于ReceivePort.listener
。当此端口收到消息时,将调用该函数。 - 在处理程序函数中,调用
connection.complete()。
此方法需要一条带有ReceivePort
和SendPort
作为参数的记录 。SendPort
是从 worker isolate 发送的初始消息,将在下一步中将其分配给名为_commands
的类级别SendPort
。 - 然后,使用
ReceivePort.fromRawReceivePort
构造函数创建一个新的ReceivePort
,并传入initPort
。
class Worker {
final SendPort _commands;
final ReceivePort _responses;
static Future<Worker> spawn() async {
// Create a receive port and add its initial message handler.
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
}
}
通过先创建一个 RawReceivePort
,然后创建一个 ReceivePort
,你稍后将能够向 ReceivePort.listen
添加新的回调。相反,如果要立即创建 ReceivePort
,则只能添加一个侦听器
,因为 ReceivePort
实现 Stream,而不是 BroadcastStream。
实际上,这允许您将 isolate 启动逻辑与设置 communication 完成后处理接收消息的 logic 分开。随着其他方法中的 logic 的增长,这种好处将变得更加明显。
Step 3: Spawn a worker isolate with Isolate.spawn
第 3 步:使用 Isolate.spawn
生成一个 worker isolate
此步骤继续填充 Worker.spawn
方法。您将添加生成 isolate 所需的代码,并从该类返回 Worker
的实例。在此示例中,对 Isolate.spawn
的调用包装在 try/catch 块中,这可确保在 isolate 启动失败时,initPort
将关闭,并且不会创建 Worker
对象。
- 首先,尝试在
try
/catch
块中生成一个 worker isolate。如果生成 worker isolate 失败,请关闭在上一步中创建的接收端口。传递给Isolate.spawn
的方法将在后续步骤中介绍。 - 接下来,等待
connection.future
,并从它返回的记录中解构 send port 和 receive port。 - 最后,通过调用其私有构造函数并传入来自该完成器的端口,返回
Worker
的实例。
class Worker {
final SendPort _commands;
final ReceivePort _responses;
static Future<Worker> spawn() async {
// Create a receive port and add its initial message handler
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
// Spawn the isolate.
try {
await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
} on Object {
initPort.close();
rethrow;
}
final (ReceivePort receivePort, SendPort sendPort) =
await connection.future;
return Worker._(receivePort, sendPort);
}
}
请注意,在此示例中(与上一个示例相比),Worker.spawn
充当此类的异步静态构造函数,并且是 创建 Worker
的实例。这简化了 API,使创建 Worker
实例的代码更加简洁。
Step 4: Complete the isolate setup process
第 4 步:完成隔离设置过程
在此步骤中,您将完成基本的隔离设置过程。这几乎完全与前面的示例相关,并且没有新概念。有一个细微的变化,代码被分解为更多方法,这是一种设计实践,可让您通过此示例的其余部分添加更多功能。有关设置 isolate 的基本过程的深入演练,请参阅基本端口示例 。
首先,创建从 Worker.spawn
返回的私有构造函数 方法。在构造函数主体中,将侦听器添加到 main isolate 的 intent 中,并将一个尚未定义的方法传递给该侦听器 称为 _handleResponsesFromIsolate
。
class Worker {
final SendPort _commands;
final ReceivePort _responses;
Worker._(this._responses, this._commands) {
_responses.listen(_handleResponsesFromIsolate);
}
}
接下来,将代码添加到负责初始化 worker isolate 上的端口的 _startRemoteIsolate
。 回想一下 ,这个方法在 Worker.spawn
方法中被传递给 Isolate.spawn
,它将作为参数传递给主 isolate 的 SendPort
。
- 创建新的
ReceivePort
。 - 将该端口的
SendPort
发送回主 isolate。 - 调用名为
_handleCommandsToIsolate
的新方法,并将主隔离中的新ReceivePort
和SendPort
作为参数传递。
static void _startRemoteIsolate(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
_handleCommandsToIsolate(receivePort, sendPort);
}
接下来,添加 _handleCommandsToIsolate
方法,该方法负责从主 isolate 接收消息,在 worker isolate 上解码 json,并将解码后的 json 作为响应发送回去。
- 首先,在 worker isolate 的
ReceivePort
上声明一个侦听器。 - 在添加到侦听器的回调中,尝试解码从 try/catch 块内的主 isolate 传递的 JSON。如果解码成功,则将解码后的 JSON 发送回主 isolate。
- 如果存在错误,请发回 RemoteError。
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
try {
final jsonData = jsonDecode(message as String);
sendPort.send(jsonData);
} catch (e) {
sendPort.send(RemoteError(e.toString(), ''));
}
});
}
接下来,添加 _handleResponsesFromIsolate
方法的代码。
- 首先,检查消息是否为
RemoteError
,在这种情况下,您应该引发
该错误。 - 否则,请打印消息。在后续步骤中,您将更新此代码以返回消息,而不是打印消息。
void _handleResponsesFromIsolate(dynamic message) {
if (message is RemoteError) {
throw message;
} else {
print(message);
}
}
最后,添加 parseJson
方法,这是一种公共方法,允许外部代码将 JSON 发送到工作程序隔离项进行解码。
Future<Object?> parseJson(String message) async {
_commands.send(message);
}
Step 5: Handle multiple messages at the same time
第 5 步:同时处理多条消息
目前,如果您快速将消息发送到工作程序 isolate,则 isolate 将按照它们完成的顺序发送解码的 json 响应,而不是按照它们发送的顺序。您无法确定哪个响应对应于哪个消息。
在此步骤中,您将通过为每条消息提供一个 id 并使用 Completer
对象来解决此问题,以确保当外部代码调用 parseJson
时,返回给该调用方的响应是正确的响应。
首先,向 Worker
添加两个类级属性:
Map<int, Completer<Object?>> _activeRequests
Map<int、completer<Object?>> _activeRequestsint _idCounter
class Worker {
final SendPort _commands;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
// ···
}
_activeRequests
映射将发送到 worker isolate 的消息与 Completer
相关联。_activeRequests
中使用的键取自 _idCounter
,随着发送的消息数增加,键数将会增加。
接下来,更新 parseJson
方法以在将消息发送到 worker isolate 之前创建完成程序。
- 首先创建一个
Completer
。 - 接下来,递增
_idCounter
,以便每个Completer
都与一个唯一的编号相关联。 - 向
_activeRequests
映射中添加一个条目,其中 key 是当前_idCounter
数,completer 是值。 - 将消息连同 id 一起发送到 worker isolate。由于您只能通过
SendPort
发送一个值,因此请将 id 和 message 包装在 记录 。 - 最后,返回 completer 的 future,它最终将包含来自 worker isolate 的响应。
Future<Object?> parseJson(String message) async {
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, message));
return await completer.future;
}
您还需要更新 _handleResponsesFromIsolate
_handleCommandsToIsolate
处理这个系统。
在 _handleCommandsToIsolate
中,您需要将消息
视为具有两个值的记录,而不仅仅是 json 文本。为此,请解构 message
中的值。
然后,在解码 json 后,更新对 sendPort.send
的调用,以再次使用记录将 id 和解码的 json 传递回主隔离。
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
final (int id, String jsonText) = message as (int, String); // New
try {
final jsonData = jsonDecode(jsonText);
sendPort.send((id, jsonData)); // Updated
} catch (e) {
sendPort.send((id, RemoteError(e.toString(), '')));
}
});
}
最后,更新 _handleResponsesFromIsolate
。
- 首先,再次从 message 参数中解构 id 和响应。
- 然后,从
_activeRequests
映射中删除与此请求对应的补全器。 - 最后,不要引发错误或打印解码的 json,而是完成完成器,传入响应。完成后,响应将返回到在主 isolate 上调用
parseJson
的代码。
void _handleResponsesFromIsolate(dynamic message) {
final (int id, Object? response) = message as (int, Object?); // New
final completer = _activeRequests.remove(id)!; // New
if (response is RemoteError) {
completer.completeError(response); // Updated
} else {
completer.complete(response); // Updated
}
}
Step 6: Add functionality to close the ports
步骤 6:添加关闭端口的功能
当您的代码不再使用 isolate 时,您应该关闭主 isolate 和 worker isolate 上的端口。
- 首先,添加一个类级布尔值,用于跟踪端口是否关闭。
- 然后,添加
Worker.close
方法。在此方法中:- 将
_closed
更新为 true。 - 向 worker isolate 发送最后一条消息。此消息是一个读取 “shutdown” 的
String
,但它可以是您想要的任何对象。您将在下一个代码片段中使用它。
- 将
- 最后,检查
_activeRequests
是否为空。如果是,请关闭主隔离的名为_responses
的ReceivePort
。
class Worker {
bool _closed = false;
// ···
void close() {
if (!_closed) {
_closed = true;
_commands.send('shutdown');
if (_activeRequests.isEmpty) _responses.close();
print('--- port closed --- ');
}
}
}
- 接下来,您需要在 worker isolate 中处理 “shutdown” 消息。将以下代码添加到
_handleCommandsToIsolate
方法中。此代码将检查消息是否为读取 “shutdown” 的String
。如果是,它将关闭 worker isolate 的ReceivePort
并返回。
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
// New if-block.
if (message == 'shutdown') {
receivePort.close();
return;
}
final (int id, String jsonText) = message as (int, String);
try {
final jsonData = jsonDecode(jsonText);
sendPort.send((id, jsonData));
} catch (e) {
sendPort.send((id, RemoteError(e.toString(), '')));
}
});
}
- 最后,您应该添加代码以在尝试发送消息之前检查端口是否已关闭。在
Worker.parseJson
方法中添加一行。
Future<Object?> parseJson(String message) async {
if (_closed) throw StateError('Closed'); // New
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, message));
return await completer.future;
}
Complete example 完整示例
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
void main() async {
final worker = await Worker.spawn();
print(await worker.parseJson('{"key":"value"}'));
print(await worker.parseJson('"banana"'));
print(await worker.parseJson('[true, false, null, 1, "string"]'));
print(
await Future.wait([worker.parseJson('"yes"'), worker.parseJson('"no"')]),
);
worker.close();
}
class Worker {
final SendPort _commands;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
bool _closed = false;
Future<Object?> parseJson(String message) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, message));
return await completer.future;
}
static Future<Worker> spawn() async {
// Create a receive port and add its initial message handler
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
// Spawn the isolate.
try {
await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
} on Object {
initPort.close();
rethrow;
}
final (ReceivePort receivePort, SendPort sendPort) =
await connection.future;
return Worker._(receivePort, sendPort);
}
Worker._(this._responses, this._commands) {
_responses.listen(_handleResponsesFromIsolate);
}
void _handleResponsesFromIsolate(dynamic message) {
final (int id, Object? response) = message as (int, Object?);
final completer = _activeRequests.remove(id)!;
if (response is RemoteError) {
completer.completeError(response);
} else {
completer.complete(response);
}
if (_closed && _activeRequests.isEmpty) _responses.close();
}
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
if (message == 'shutdown') {
receivePort.close();
return;
}
final (int id, String jsonText) = message as (int, String);
try {
final jsonData = jsonDecode(jsonText);
sendPort.send((id, jsonData));
} catch (e) {
sendPort.send((id, RemoteError(e.toString(), '')));
}
});
}
static void _startRemoteIsolate(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
_handleCommandsToIsolate(receivePort, sendPort);
}
void close() {
if (!_closed) {
_closed = true;
_commands.send('shutdown');
if (_activeRequests.isEmpty) _responses.close();
print('--- port closed --- ');
}
}
}