Isolate是Dart中重要的异步通信方式,它的作用和用法不再赘述,关键是怎样优雅的与它进行数据通信。这里实践了一种写法,可以按这种写法结合自己的工程比较优雅地完成数据通信。
一般写法
在Dart中与ioslate通信的一般用这种模式,也可看官方的示例代码 :
dart
import 'dart:isolate';
void main() async {
final recv = ReceivePort('main.incoming');
final isolate = await Isolate.spawn<SendPort>(
IsolateObj.setupIsolate,
recv.sendPort,
);
final main = MainObj();
recv.listen(main.handleResponsesFromIsolate);
// call it somewhere
main.create();
...
...
recv.close();
isolate.kill();
}
class MainObj {
void handleResponsesFromIsolate(dynamic msg) {
}
void create() {
_send?.send({
'type': 'create',
});
}
}
class IsolateObj {
static void setupIsolate(SendPort outgoing) async {
final incoming = ReceivePort('_isolate.incoming');
outgoing.send(incoming.sendPort);
}
}
在Isolate中,响应每个消息的动作可能是不同的,于是不得不有一个switch case
,如果消息的类型非常多则分支十分巨大。不管这个分支是针对数据类型的还是数据字段的,针对每个分支不得不新增很多处理方法:
dart
class IsolateObj {
final SendPort outgoing;
IsolateObj(this.outgoing);
void _create(Map<String, dynamic> msg) {
}
void _dispose(Map<String, dynamic> msg) {
}
void _handleMessageFromMain(Map<String, dynamic> msg) {
final type = msg['type'];
switch (type) {
case 'create':
_create(msg);
break;
case 'dispose':
_dispose(msg);
break;
}
}
static void setupIsolate(SendPort outgoing) async {
final incoming = ReceivePort('_isolate.incoming');
outgoing.send(incoming.sendPort);
final messages = incoming.cast<Map<String, dynamic>>();
final obj = IsolateObj(outgoing);
messages.listen(obj._handleMessageFromMain);
}
}
另一个别扭的地方是ReceivePort
中接收的数据类型 是不同的,第一个数据元素类型是SendPort
,要让接收方能够发送数据,其后的元素才能是真正通信的数据类型。
dart
void handleResponsesFromIsolate(dynamic msg) {
if (msg is SendPort) {
} else if (msg is Map<String, dynamic>) {
}
}
在Isolate中处理完消息,可能需要再给主线程一些"反馈",也就是说主线程除了"发送"数据外也要"接收"数据,来实现"双向通信"。Dart中发送和接收这两个操作是分开的,需要分别用SendPort
和ReceivePort
。而一旦接收数据,主线程中也不得不建立一个巨大的switch case
:
dart
class IsolateObj {
final SendPort outgoing;
IsolateObj(this.outgoing);
void _create(Map<String, dynamic> msg) {
// do some time consuming creation operation
outgoing.send({
'type': 'created',
});
}
void _dispose(Map<String, dynamic> msg) {
// do some dipose
outgoing.send({
'type': 'created',
});
}
void _handleMessageFromMain(Map<String, dynamic> msg) {
final type = msg['type'];
switch (type) {
case 'create':
_create(msg);
break;
case 'dispose':
_dispose(msg);
break;
}
}
}
class MainObj {
void handleResponsesFromIsolate(dynamic msg) {
if (msg is SendPort) {
} else if (msg is Map<String, dynamic>) {
switch (msg['type']) {
case 'created':
_onCreated(msg);
break;
case 'disposed':
break;
}
}
}
void create() {
_send?.send({
'type': 'create',
});
}
void _onCreated(msg) {
final data = msg['data'];
}
}
显然这种写法的第三个缺点是上下文割裂 。发送处和接收处位于不同的方法体内,很多时候不得不再存储额外的上下文信息。因为时序的原因,一些类成员也不得不声明成可空类型,比如SendPort?
:
dart
class MainObj {
SendPort? _send;
void handleResponsesFromIsolate(dynamic msg) {
if (msg is SendPort) {
_send = msg;
} else if (msg is Map<String, dynamic>) {
}
}
}
上下文关联
既然Isolate通信是用一种"通道",我们能不能像http请求那样发送完请求之后直接"等待"当次请求的返回?就像这样:
dart
class MainObj {
Future<void> create() async {
_send?.send({
'type': 'create',
});
final msg = await getNextElementFromIsolate();
final data = msg['data'];
}
}
这样主线程就可以在主线程移除大switch case
,而且create
和onCreated
可以合并起来。我们可以发现ReceivePort
其实就是一个Stream
。可以满足获取下一个元素的方法只有Stream.first
,而且只要能获取下一个Stream
元素,SendPort
可以避免成为可空类型:
dart
final isolate = await Isolate.spawn<SendPort>(
IsolateObj.setupIsolate,
recv.sendPort,
);
final port = await recv.first;
final data = recv.cast<Map<String, dynamic>>();
final main = MainObj(data, port);
class MainObj {
final Stream<Map<String, dynamic>> _data;
final SendPort _send;
MainObj(this._data, this._send);
Future<void> create() async {
_send.send({
'type': 'create',
});
final msg = await _data.first;
final data = msg['data'];
}
}
太棒了,一下子大大简化了主线程这一侧的操作逻辑!
但是!别高兴太早。这个实现有个很大的问题。
监听时序
我们的意图其实很明确,发送一次请求,接收这次请求的结果,也就是请求和结果一对一。Stream
中的元素应该一个接一个的接收,上一个元素被消费完,下一个元素再接收,否则结果就会紊乱。然而问题就在Stream.first
中的实现仅仅是加了一个linstener,等同于Stream.elementAt(0)
。多次调用Stream.first
,只是注册多次监听,当一个元素数据来了结果只是触发多次通知,这并不是我们期望的"一对一"。也就是说:
dart
final results = [
await _data.first,
await _data.first,
]; // [element1, element2]
这样写是结果是对的,在接收了一个元素之后再去等待下一个元素,但同时等待并行的元素,那结果则是错误的:
dart
final results = await Future.wait([
_data.first,
_data.first,
]); // [element1, element1]
实际开发中,第二种情况才是最多的,比如在main.create()
调用之后再一次调用了main.create()
,这时候数据还没有在isolate中处理,等处理完之后内部接收的其实是相同的数据。
流式队列
要解决这个问题,必须得用新的方法。目的很明确:在流中一个一个的获取元素对象。多亏了美妙的StreamQueue
,已经帮我们达到这个目的。StreamQueue
放在package:async/async.dart
中。只需要一点点更改:
dart
final port = await recv.first;
final data = recv.cast<Map<String, dynamic>>();
final main = MainObj(StreamQueue(data), port);
class MainObj {
final StreamQueue<Map<String, dynamic>> _data;
final SendPort _send;
MainObj(this._data, this._send);
Future<void> create() async {
_send.send({
'type': 'create',
});
final msg = await _data.next;
final data = msg['data'];
}
}
轻松搞定!其实用一个简单的List<Completer<Map<String, dynamic>>
就能够实现以上的效果,有兴趣的朋友可以尝试一下!
被动通知
现实情况永远是更复杂的,在异步通信中,可不是只有主动请求的情况,还有一种被动通知 的情况。这和网络请求是类似的,ReceivePort
类似一个长连接,在长连接建立起来之后,服务端很可能会主动推送一些事件,从而客户端被接收通知。结果这样可能会造成乱序,本来是这样:
lua
main isolate
create -----------------> _create
create -----------------> _create
onCreated <-------------- data1
onCreated <-------------- data2
但如果混杂了被动通知:
lua
main isolate
create -----------------> _create
create -----------------> _create
onCreated <-----notify--- data3
onCreated <-------------- data1
? <-------------- data2
所以问题的关键是我们预设了接收的响应对应最近一次发送的请求,典型的客户端服务端模式。但实际情况不是这样,Isolate虽然是执行主线程发出的指令,但在执行的过程中可能会触发某个条件去通知主线程,在这种情况下,主线程接收的可能都是错误的结果。
要解决这个问题其实也非常简单。既然被动通知的消息会造成时序紊乱,那把它从流中过滤出来单独形成一个流就可以了。
ini
final port = await recv.first;
final receiving = recv.cast<Map<String, dynamic>>().asBroadcastStream();
final data = receiving.where((e) => e['type'] != 'notify');
final notify = receiving.where((e) => e['type'] == 'notify');
final main = MainObj(StreamQueue(data), port);
notify.listen((e) {
// do something on notified.
});
到这里,乱序问题其实已经解决了。但这里给出另一种方法,既然被动通知型消息需要从流中剔除,那干脆就不要放进这个流里------谁说只能有一个通道的?我们知道ReceivePort
其实就是一个Stream,那针对notify消息,单独开设一个通道就好,最终代码如下:
dart
import 'dart:isolate';
import 'package:async/async.dart' show StreamQueue;
void main() async {
final recv = ReceivePort('main.incoming');
final notify = ReceivePort('notify.incoming');
final isolate = await Isolate.spawn<(SendPort, SendPort)>(
IsolateObj.setupIsolate,
(recv.sendPort, notify.sendPort),
);
final port = await recv.first;
final receiving = recv.cast<Map<String, dynamic>>().asBroadcastStream();
final main = MainObj(StreamQueue(receiving), port);
notify.cast<Map<String, dynamic>>().listen(main.onNotified);
await main.create();
recv.close();
isolate.kill();
}
class MainObj {
final StreamQueue<Map<String, dynamic>> _data;
final SendPort _send;
MainObj(this._data, this._send);
Future<void> create() async {
_send.send({
'type': 'create',
});
final msg = await _data.next;
final data = msg['data'];
// continue to do something.
}
void onNotified(Map<String, dynamic> data) {
}
}
class IsolateObj {
final SendPort outgoing;
final SendPort notify;
IsolateObj(this.outgoing, this.notify);
void _create(Map<String, dynamic> msg) {
// do some creation
outgoing.send({
'type': 'created',
});
}
void _dispose(Map<String, dynamic> msg) {
}
void _notifyCallback() {
notify.send({
'type': 'notify',
});
}
void _handleMessageFromMain(Map<String, dynamic> msg) {
final type = msg['type'];
switch (type) {
case 'create':
_create(msg);
break;
case 'dispose':
_dispose(msg);
break;
}
}
static void setupIsolate((SendPort, SendPort) r) async {
final (outgoing, notify) = r;
final incoming = ReceivePort('_isolate.incoming');
outgoing.send(incoming.sendPort);
final messages = incoming.cast<Map<String, dynamic>>();
final obj = IsolateObj(outgoing, notify);
messages.listen(obj._handleMessageFromMain);
}
}
多亏了Dart3中的Record,传送多参数现在非常简单了!
这种写法可以作为Isolate通信的一种新范式,和各种switch case
说再见吧!