Dart: Isolate通信新范式

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中发送和接收这两个操作是分开的,需要分别用SendPortReceivePort。而一旦接收数据,主线程中也不得不建立一个巨大的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,而且createonCreated可以合并起来。我们可以发现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说再见吧!

相关推荐
Kika写代码1 分钟前
【基于轻量型架构的WEB开发】课程 13.2.4 拦截器 Java EE企业级应用开发教程 Spring+SpringMVC+MyBatis
spring·架构·java-ee
liang899914 分钟前
设计模式之策略模式(Strategy)
设计模式·策略模式
马剑威(威哥爱编程)1 小时前
读写锁分离设计模式详解
java·设计模式·java-ee
修道-03231 小时前
【JAVA】二、设计模式之策略模式
java·设计模式·策略模式
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
CodingBrother3 小时前
软考之面向服务架构SOA-通信方法
架构
码哥字节4 小时前
重生之从零设计 MySQL 架构
数据库·mysql·架构
G皮T5 小时前
【设计模式】结构型模式(四):组合模式、享元模式
java·设计模式·组合模式·享元模式·composite·flyweight
W_Meng_H5 小时前
设计模式-组合模式
设计模式·组合模式
wclass-zhengge13 小时前
系统架构(01架构的特点,本质...)
架构·系统架构