Dart 2.15 更新后 isolate 應(yīng)該這么用

序言

2021年的最后一天, Dart 官方發(fā)布了 dart 2.15 版本,該版本優(yōu)化了很多內(nèi)容,今天我們要重點(diǎn)說(shuō)說(shuō) isolate 工作器。官方推文鏈接

在探索新變化之前,我們來(lái)回憶鞏固一下 isolate 的使用。

isolate 的作用

問(wèn)題:Flutter 基于單線程模式使用協(xié)程進(jìn)行開(kāi)發(fā),為什么還需要 isolate

首先我們要明確 并行(isolate)并發(fā)(future)的區(qū)別。下面我們通過(guò)簡(jiǎn)單的例子來(lái)進(jìn)行說(shuō)明 。Demo 是一個(gè)簡(jiǎn)單的頁(yè)面,中間放置一個(gè)不斷轉(zhuǎn)圈的 progress 和一個(gè)按鍵,按鍵用來(lái)觸發(fā)耗時(shí)方法。

///計(jì)算偶數(shù)個(gè)數(shù)(具體的耗時(shí)操作)下面示例代碼中會(huì)用到
static int calculateEvenCount(int num) {
    int count = 0;
    while (num > 0) {
      if (num % 2 == 0) {
        count++;
      }
      num--;
    }
    return count;
  }
///按鍵點(diǎn)擊事件
onPressed: () {
      //觸發(fā)耗時(shí)操作
      doMockTimeConsume();
  }
  • 方式一: 我們將耗時(shí)操作使用 future 的方式進(jìn)行封裝
///使用future的方式封裝耗時(shí)操作
static Future<int> futureCountEven(int num) async {
    var result = calculateEvenCount(num);
    return Future.value(result);
  }

///耗時(shí)事件
void doMockTimeConsume() async {
    var result = await futureCountEven(1000000000);
    _count = result;
    setState(() {});
  }

結(jié)果如下:


結(jié)論:使用 future 的方式來(lái)消費(fèi)耗時(shí)操作,由于仍然是單線程在進(jìn)行工作,異步只是在同一個(gè)線程的并發(fā)操作,仍會(huì)阻塞UI的刷新。

  • 方式二: 使用 isolate 開(kāi)辟新線程,避開(kāi)主線程,不干擾UI刷新
//模擬耗時(shí)操作
  void doMockTimeConsume() async {
    var result = await isolateCountEven(1000000000);
    _count = result;
    setState(() {});
  }
  ///使用isolate的方式封裝耗時(shí)操作
  static Future<dynamic> isolateCountEven(int num) async {
    final p = ReceivePort();
    ///發(fā)送參數(shù)
    await Isolate.spawn(_entryPoint, [p.sendPort, num]);
    return (await p.first) as int;
  }

 static void _entryPoint(List<dynamic> args) {
    SendPort responsePort = args[0];
    int num = args[1];
    ///接收參數(shù),進(jìn)行耗時(shí)操作后返回?cái)?shù)據(jù)
    responsePort.send(calculateEvenCount(num));
  }

結(jié)果如下:


結(jié)論:使用 isolate 實(shí)現(xiàn)了多線程并行,在新線程中進(jìn)行耗時(shí)操作不會(huì)干擾UI線程的刷新。

isolate 的局限性,為什么需要優(yōu)化?

iso 有兩點(diǎn)較為重要的局限性。

  • isolate 消耗較重,除了創(chuàng)建耗時(shí),每次創(chuàng)建還至少需要2Mb的空間,有OOM的風(fēng)險(xiǎn)。
  • isolate 之間的內(nèi)存空間各自獨(dú)立,當(dāng)參數(shù)或結(jié)果跨 iso 相互傳遞時(shí)需要深度拷貝,拷貝耗時(shí),可能造成UI卡頓。

isolate 新特性

Dart 2.15 更新, 給 iso 添加了組的概念,isolate 組 工作特征可簡(jiǎn)單總結(jié)為以下兩點(diǎn):

  • Isolate 組中的 isolate 共享各種內(nèi)部數(shù)據(jù)結(jié)構(gòu)
  • Isolate 組仍然阻止在 isolate 間共享訪問(wèn)可變對(duì)象,但由于 isolate 組使用共享堆實(shí)現(xiàn),這也讓其擁有了更多的功能。

官方推文中舉了一個(gè)例子

工作器 isolate 通過(guò)網(wǎng)絡(luò)調(diào)用獲得數(shù)據(jù),將該數(shù)據(jù)解析為大型 JSON 對(duì)象圖,然后將這個(gè) JSON 圖返回到主 isolate 中。

Dart 2.15 之前:執(zhí)行該操作需要深度復(fù)制,如果復(fù)制花費(fèi)的時(shí)間超過(guò)幀預(yù)算時(shí)間,就會(huì)導(dǎo)致界面卡頓。

使用 Dart 2.15:工作器 isolate 可以調(diào)用 Isolate.exit(),將其結(jié)果作為參數(shù)傳遞。然后,Dart 運(yùn)行時(shí)將包含結(jié)果的內(nèi)存數(shù)據(jù)從工作器 isolate 傳遞到主 isolate 中,無(wú)需復(fù)制,且主 isolate 可以在固定時(shí)間內(nèi)接收結(jié)果。

重點(diǎn):提供 Isolate.exit() 方法,將包含結(jié)果的內(nèi)存數(shù)據(jù)從工作器 isolate 傳遞到主 isolate ,過(guò)程無(wú)需復(fù)制。

附注: 使用 Dart 新特性,需將 flutter sdk 升級(jí)到 2.8.0 以上 鏈接

exit 和 send 的區(qū)別及用法

Dart 更新后,我們將數(shù)據(jù)從 工作器 isolate(子線程)回傳到 主 isolate(主線程)有兩種方式。

  • 方式一: 使用 send
responsePort.send(data);

點(diǎn)擊進(jìn)入 send 方法查看源碼注釋,看到這樣一句話:

結(jié)論:send 本身不會(huì)阻塞,會(huì)立即發(fā)送,但可能需要線性時(shí)間成本用于復(fù)制數(shù)據(jù)。

  • 方式二:使用 exit
Isolate.exit(responsePort, data);

官網(wǎng) 給出的解釋如下:

結(jié)論:隔離之間的消息傳遞通常涉及數(shù)據(jù)復(fù)制,因此可能會(huì)很慢,并且會(huì)隨著消息大小的增加而增加。但是 exit(),則是在退出隔離中保存消息的內(nèi)存,不會(huì)被復(fù)制,而是被傳輸?shù)街?isolate。這種傳輸很快,并且在恒定的時(shí)間內(nèi)完成。

我們把上面 demo 中的 _entryPoint 方法做一下優(yōu)化修改:

 static void _entryPoint(SendPort port) {
    SendPort responsePort = args[0];
    int num = args[1];
    ///接收參數(shù),進(jìn)行耗時(shí)操作后返回?cái)?shù)據(jù)
    //responsePort.send(calculateEvenCount(num));
    Isolate.exit(responsePort, calculateEvenCount(num));
  }

總結(jié):使用 exit() 替代 SendPort.send,可規(guī)避數(shù)據(jù)復(fù)制,節(jié)省耗時(shí)。

isolate 組

如何創(chuàng)建一個(gè) isolate 組?官方給出的解釋如下:

When an isolate calls Isolate.spawn(), the two isolates have the same executable code and are in the same isolate group. Isolate groups enable performance optimizations such as sharing code; a new isolate immediately runs the code owned by the isolate group. Also, Isolate.exit() works only when the isolates are in the same isolate group.

當(dāng)在 isolate 中調(diào)用另一個(gè) isolate 時(shí),這兩個(gè) isolate 具有相同的可執(zhí)行代碼,并且位于同一隔離組。
PS: 小轟暫時(shí)也沒(méi)有想到具體的使用場(chǎng)景,先暫放一邊吧。

實(shí)踐:isolate 如何處理連續(xù)數(shù)據(jù)

結(jié)合上面的耗時(shí)方法calculateEvenCountisolate 處理連續(xù)數(shù)據(jù)需要結(jié)合 stream 流 的設(shè)計(jì)。具體 demo 如下:

///測(cè)試入口
static testContinuityIso() async {
    final numbs = [10000, 20000, 30000, 40000];
    await for (final data in _sendAndReceive(numbs)) {
      log(data.toString());
    }
  }
///具體的iso實(shí)現(xiàn)(主線程)
static Stream<Map<String, dynamic>> _sendAndReceive(List<int> numbs) async* {
    final p = ReceivePort();
    await Isolate.spawn(_entry, p.sendPort);
    final events = StreamQueue<dynamic>(p);

    // 拿到 子isolate傳遞過(guò)來(lái)的 SendPort 用于發(fā)送數(shù)據(jù)
    SendPort sendPort = await events.next;
    for (var num in numbs) {
      //發(fā)送一條數(shù)據(jù),等待一條數(shù)據(jù)結(jié)果,往復(fù)循環(huán)
      sendPort.send(num);
      Map<String, dynamic> message = await events.next;
      //每次的結(jié)果通過(guò)stream流外露
      yield message;
    }
    //發(fā)送 null 作為結(jié)束標(biāo)識(shí)符
    sendPort.send(null);
    await events.cancel();
  }
///具體的iso實(shí)現(xiàn)(子線程)
static Future<void> _entry(SendPort p) async {
    final commandPort = ReceivePort();
    //發(fā)送一個(gè) sendPort 給主iso ,用于 主iso 發(fā)送參數(shù)給 子iso
    p.send(commandPort.sendPort);
    await for (final message in commandPort) {
      if (message is int) {
        final data = calculateEvenCount(message);
        p.send(data);
      } else if (message == null) {
        break;
      }
    }
  }

拋磚引玉,這只是一個(gè)思路~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容