Dart 語言非同步編程之Isolate

  • 2019 年 10 月 4 日
  • 筆記
  • 非同步編程之Isolate
    • spawnUri
    • spawn
    • Flutter 中創建Isolate
    • 使用場景

非同步編程之Isolate

之前的文章已經說過,將非常耗時的任務添加到事件隊列後,仍然會拖慢整個事件循環的處理,甚至是阻塞。可見基於事件循環的非同步模型仍然是有很大缺點的,這時候我們就需要Isolate,這個單詞的中文意思是隔離。

簡單說,可以把它理解為Dart中的執行緒。但它又不同於執行緒,更恰當的說應該是微執行緒,或者說是協程。它與執行緒最大的區別就是不能共享記憶體,因此也不存在鎖競爭問題,兩個Isolate完全是兩條獨立的執行線,且每個Isolate都有自己的事件循環,它們之間只能通過發送消息通訊,所以它的資源開銷低於執行緒。

從主Isolate創建一個新的Isolate有兩種方法

spawnUri

static Future<Isolate> spawnUri()

spawnUri方法有三個必須的參數,第一個是Uri,指定一個新Isolate程式碼文件的路徑,第二個是參數列表,類型是List<String>,第三個是動態消息。需要注意,用於運行新Isolate的程式碼文件中,必須包含一個main函數,它是新Isolate的入口方法,該main函數中的args參數列表,正對應spawnUri中的第二個參數。如不需要向新Isolate中傳參數,該參數可傳空List

Isolate中的程式碼:

import 'dart:isolate';      void main() {    print("main isolate start");    create_isolate();    print("main isolate stop");  }    // 創建一個新的 isolate  create_isolate() async{    ReceivePort rp = new ReceivePort();    SendPort port1 = rp.sendPort;      Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1);      SendPort port2;    rp.listen((message){      print("main isolate message: $message");      if (message[0] == 0){        port2 = message[1];      }else{        port2?.send([1,"這條資訊是 main isolate 發送的"]);      }    });      // 可以在適當的時候,調用以下方法殺死創建的 isolate    // newIsolate.kill(priority: Isolate.immediate);  }  

創建other_task.dart文件,編寫新Isolate的程式碼

import 'dart:isolate';  import  'dart:io';      void main(args, SendPort port1) {    print("isolate_1 start");    print("isolate_1 args: $args");      ReceivePort receivePort = new ReceivePort();    SendPort port2 = receivePort.sendPort;      receivePort.listen((message){      print("isolate_1 message: $message");    });      // 將當前 isolate 中創建的SendPort發送到主 isolate中用於通訊    port1.send([0, port2]);    // 模擬耗時5秒    sleep(Duration(seconds:5));    port1.send([1, "isolate_1 任務完成"]);      print("isolate_1 stop");  }  

運行主Isolate的結果:

main isolate start  main isolate stop  isolate_1 start  isolate_1 args: [hello, isolate, this is args]  main isolate message: [0, SendPort]  isolate_1 stop  main isolate message: [1, isolate_1 任務完成]  isolate_1 message: [1, 這條資訊是 main isolate 發送的]  

整個消息通訊過程如上圖所示,兩個Isolate是通過兩對Port對象通訊,一對Port分別由用於接收消息的ReceivePort對象,和用於發送消息的SendPort對象構成。其中SendPort對象不用單獨創建,它已經包含在ReceivePort對象之中。需要注意,一對Port對象只能單向發消息,這就如同一根自來水管,ReceivePortSendPort分別位於水管的兩頭,水流只能從SendPort這頭流向ReceivePort這頭。因此,兩個Isolate之間的消息通訊肯定是需要兩根這樣的水管的,這就需要兩對Port對象。

理解了Isolate消息通訊的原理,那麼在Dart程式碼中,具體是如何操作的呢?

ReceivePort對象通過調用listen方法,傳入一個函數可用來監聽並處理髮送來的消息。SendPort對象則調用send()方法來發送消息。send方法傳入的參數可以是null,num, bool, double,String, List ,Map或者是自定義的類。 在上例中,我們發送的是包含兩個元素的List對象,第一個元素是整型,表示消息類型,第二個元素則表示消息內容。

spawn

static Future<Isolate> spawn()

除了使用spawnUri,更常用的是使用spawn方法來創建新的Isolate,我們通常希望將新創建的Isolate程式碼和main Isolate程式碼寫在同一個文件,且不希望出現兩個main函數,而是將指定的耗時函數運行在新的Isolate,這樣做有利於程式碼的組織和程式碼的復用。spawn方法有兩個必須的參數,第一個是需要運行在新Isolate的耗時函數,第二個是動態消息,該參數通常用於傳送主IsolateSendPort對象。

spawn的用法與spawnUri相似,且更為簡潔,將上面例子稍作修改如下

import 'dart:isolate';  import  'dart:io';    void main() {    print("main isolate start");    create_isolate();    print("main isolate end");  }    // 創建一個新的 isolate  create_isolate() async{    ReceivePort rp = new ReceivePort();    SendPort port1 = rp.sendPort;      Isolate newIsolate = await Isolate.spawn(doWork, port1);      SendPort port2;    rp.listen((message){      print("main isolate message: $message");      if (message[0] == 0){        port2 = message[1];      }else{        port2?.send([1,"這條資訊是 main isolate 發送的"]);      }    });  }    // 處理耗時任務  void doWork(SendPort port1){    print("new isolate start");    ReceivePort rp2 = new ReceivePort();    SendPort port2 = rp2.sendPort;      rp2.listen((message){      print("doWork message: $message");    });      // 將新isolate中創建的SendPort發送到主isolate中用於通訊    port1.send([0, port2]);    // 模擬耗時5秒    sleep(Duration(seconds:5));    port1.send([1, "doWork 任務完成"]);      print("new isolate end");  }  

運行結果:

main isolate start  main isolate end  new isolate start  main isolate message: [0, SendPort]  new isolate end  main isolate message: [1, doWork 任務完成]  doWork message: [1, 這條資訊是 main isolate 發送的]  

無論是上面的spawn還是spawnUri,運行後都會創建兩個進程,一個是主Isolate的進程,一個是新Isolate的進程,兩個進程都雙向綁定了消息通訊的通道,即使新的Isolate中的任務完成了,它的進程也不會立刻退出,因此,當使用完自己創建的Isolate後,最好調用newIsolate.kill(priority: Isolate.immediate);Isolate立即殺死。

Flutter 中創建Isolate

無論如何,在Dart中創建一個Isolate都顯得有些繁瑣,可惜的是Dart官方並未提供更高級封裝。但是,如果想在Flutter中創建Isolate,則有更簡便的API,這是由Flutter官方進一步封裝ReceivePort而提供的更簡潔API。詳細API文檔[1]

使用compute函數來創建新的Isolate並執行耗時任務

import 'package:flutter/foundation.dart';  import  'dart:io';    // 創建一個新的Isolate,在其中運行任務doWork  create_new_task() async{    var str = "New Task";    var result = await compute(doWork, str);    print(result);  }      String doWork(String value){    print("new isolate doWork start");    // 模擬耗時5秒    sleep(Duration(seconds:5));      print("new isolate doWork end");    return "complete:$value";  }  

compute函數有兩個必須的參數,第一個是待執行的函數,這個函數必須是一個頂級函數,不能是類的實例方法,可以是類的靜態方法,第二個參數為動態的消息類型,可以是被運行函數的參數。需要注意,使用compute應導入'package:flutter/foundation.dart'包。

使用場景

Isolate雖好,但也有合適的使用場景,不建議濫用Isolate,應儘可能多的使用Dart中的事件循環機制去處理非同步任務,這樣才能更好的發揮Dart語言的優勢。

那麼應該在什麼時候使用Future,什麼時候使用Isolate呢?一個最簡單的判斷方法是根據某些任務的平均時間來選擇:

  • 方法執行在幾毫秒或十幾毫秒左右的,應使用Future
  • 如果一個任務需要幾百毫秒或之上的,則建議創建單獨的Isolate

除此之外,還有一些可以參考的場景

  • JSON 解碼
  • 加密
  • 影像處理:比如剪裁
  • 網路請求:載入資源、圖片

參考資料:

Dart 文檔[2]

Isolate 文檔[3]

參考資料

[1]

詳細API文檔: https://docs.flutter.io/flutter/foundation/compute.html

[2]

Dart 文檔: https://webdev.dartlang.org/articles/performance/event-loop

[3]

Isolate 文檔: https://api.dartlang.org/stable/2.3.0/dart-isolate/Isolate-class.html