Dart基礎之Isolate

翔媽不會飛發表於2021-05-12

Dart基礎之Isolate

背景

在其他語言中為了高效利用多核CPU,通常使用多執行緒並行來實現併發執行程式碼,通過共享資料來保證多執行緒之間的協同,但這種模式衍生出了很多問題,開闢執行緒帶來資源消耗,資料共享代理死鎖問題。

不論是APP還是Web端,CPU大多數時間是處於空閒狀態的,一般不需要密集和高併發的處理。Dart作為面向前端開發設計的語言,在併發設計上沒有采用多執行緒方案,而是使用了Isolate(隔離區)這種單執行緒模型來解決併發任務對於多執行緒的依賴。

Isolate組成

每個Isolate由以下幾部分組成:

(1)Stack用於存放函式呼叫上下文和呼叫鏈路資訊。

(2)Heap用於存放物件,堆記憶體回收管理和java類似。

(3)用於存放非同步回撥的Queue,分為微事件佇列(MicroTaskQueue)和微任務佇列(EventQueue)。

(4)以及一個用於處理非同步回撥的EventLoop。

Isolate執行程式碼

在dart中編寫的程式碼分為兩種型別:

同步程式碼:即正常編寫的程式碼。

非同步程式碼:一些返回型別為Future和Stream的函式。

由於Isolate是一種單執行緒模型,程式碼執行時碰到非同步程式碼會將其丟到Queue中,只按順序執行同步程式碼。等同步程式碼都執行完成後才會按順序執行非同步任務。

下面寫demo驗證下這個結論,下圖的Future.delayed就是非同步程式碼。

Future printc(){
  Future.delayed(new Duration(seconds: 2),(){
    print("a");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
  });
  Future.delayed(new Duration(seconds: 1),(){
    print("b");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
  });
  print("c");
  var start = DateTime.now();
  for(int i=0;i<100000;i++){
    for(int j=0;j<100000;j++){
      i*j+j*i-j*i;
    }
  }
  var end = DateTime.now();
  print("同步耗時任務:${end.second-start.second}秒");
  print("d");
}
複製程式碼

執行結果為:

c
同步耗時任務:11秒
d
b
a
複製程式碼

解釋下結果,列印a和b的非同步任務會插入到Queue中,其回撥目前也不會執行。執行同步程式碼,所以首先列印的是c,之後等同步耗時任務執行完成後再列印d。同步程式碼執行完成後EventLoop會從Queue中提取非同步程式碼執行,雖然非同步任務a在前面,但其延時時間長,所以先列印了b。

同步等待:在呼叫非同步程式碼時(比如Future)使用await關鍵字,在方法宣告處使用async關鍵字。

如果就是想先執行非同步任務再執行同步程式碼則可以使用await來執行非同步任務並阻塞同步程式碼。非同步任務執行完畢後會繼續執行同步程式碼。

Future printc() async{
  await Future.delayed(new Duration(seconds: 2),(){
    print("a");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
  });
  await Future.delayed(new Duration(seconds: 1),(){
    print("b");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
  });
  print("c");
  var start = DateTime.now();
  for(int i=0;i<100000;i++){
    for(int j=0;j<10000;j++){
      i*j+j*i-j*i;
    }
  }
  var end = DateTime.now();
  print("同步耗時任務:${end.difference(start).inSeconds}秒");
  print("d");
}
複製程式碼

在兩個非同步任務中新增了await修飾符,則會像同步程式碼一樣按照順序進行執行。

a
b
c
同步耗時任務:1秒
d
複製程式碼

EventLoop

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Te4ssP33-1620817798651)(Dart%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B.assets/168297301229dbb9)]

非同步任務會丟到EventQueue由EventLoop來執行。會不斷的從時間佇列中獲取非同步任務並執行。

下圖展示了EventLoop從佇列中獲取任務並執行的過程。

在這裡插入圖片描述

圖中的MicrotaskQueue是微任務佇列,優先順序最高,每次迴圈都會先檢查微任務佇列,有微任務則優先執行微任務,直到所有微任務都做完後才去執行EventQueue。

Microtask:微任務一般用於執行很短的非同步操作,任務量不能太多。 生成微任務的方式有

// 方式一
scheduleMicrotask((){
  print("我是一條微任務!");
});
// 方式二
Future.microtask(() => print("我是另一條微任務!"));
複製程式碼

Event:事件,主要來自於Future,已經IO、手勢、繪製、計時器和與Isloate通訊的message等。

此處需要注意Future使用的特殊場景。

Future.delayed(new Duration(seconds: 2),(){
  print("a");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
});
複製程式碼

Future.delayed 會在延遲結束後才把非同步回撥新增到EventQueue尾部,並不是立即執行,不能保證執行時間。

  Future(()=>print("zzz"))
  .then((value) => print("xxx"))
  .then((value) => print("yyy"))
  .then((value) => print("www"));
複製程式碼

Future.then 會對非同步事件補充回撥,then不會向EventQueue中新增事件,而是在前面的Future執行完成後立即執行。可以保證多個then內部task的執行順序。

  Future aaa = Future(()=>print("aaa"));
// 此時aaa已經處於完成狀態
  aaa.then((value) => print("bbb"));
// Future(()=>null)生成的就是一個已經完成的Future
  Future(()=>null).then((value) => print("ccc"));
複製程式碼

針對已經完成的Future,呼叫then時並不會立即執行,而是將then中的回撥新增到Microtask佇列中。

案例分析:

void dartLoopTest() {
  Future x0 = Future(() => null);
  Future x = Future(() => print('1'));
  Future(() => print('2'));
  scheduleMicrotask(() => print('3'));
  x.then((value) {
    print('4');
    Future(() => print('5'));
  }).then((value) => print('6'));
  print('7');
  x0.then((zvalue) {
    print('8');
    scheduleMicrotask(() {
      print('9');
    });
  }).then((value) => print('10'));
}
複製程式碼

輸出結果為:

7 
3
8
10
9
1
4
6
2
5
複製程式碼
  1. 最先執行同步程式碼print('7');

  2. 其次判斷MicroTask佇列,執行 scheduleMicrotask(() => print('3'));

  3. 當前MicroTask佇列為空,從Event佇列中獲取事件,按照程式碼順序執行

    Future x0 = Future(() => null);
    複製程式碼

    此時沒有輸出,但x0是完成狀態的Future,第一個then的回撥會加入到MicroTask佇列。

    x0.then((zvalue) {
        print('8');
        scheduleMicrotask(() {
          print('9');
        });
      }).then((value) => print('10'));
    複製程式碼

    然後執行微任務,呼叫print('8');,執行scheduleMicrotask((){print('9');});向微任務佇列新增print('9');的任務。由於當前微任務沒有執行完,所以會先呼叫print('10')

  4. 按順序執行 Future x = Future(() => print('1'));,之後x 也處於完成狀態,then回撥會新增到微任務佇列中執行。在微任務中先列印4,再向事件佇列新增列印5的事件,最後列印6。

      x.then((value) {
        print('4');
        Future(() => print('5'));
      }).then((value) => print('6'));
    複製程式碼
  5. 此時事件佇列裡還有列印2和列印5的事件,按順序執行輸出最後結果。

void testThenAwait(){
  Future(()=>print("AAA"))
      .then((value) async => await Future(()=>print("BBB")))
      .then((value) => print("CCC"));
  Future(()=>print("DDD"));
}
複製程式碼

當我們在then中使用await時會阻塞當前then向下呼叫。

輸出結果:

AAA
DDD
BBB
CCC
複製程式碼

輸出分析:

  1. 首先這個方法中有兩個Future,按順序新增到Event佇列中。
  2. 執行第一個Future,列印AAA,
  3. 接著同步執行第一個then,向Event佇列中新增第三FutureFuture(()=>print("BBB")由於有同步等待,必須等Future(()=>print("BBB")執行完畢後才會繼續向下執行。
  4. 執行Event佇列中排在前面的非同步事件Future(()=>print("DDD"));,列印DDD
  5. 執行Event佇列中剩餘的非同步事件Future(()=>print("BBB"),列印BBB
  6. 接著執行then,列印CCC

建立Isolate

基本方法

Flutter應用中的程式碼預設都跑在root isloate 中,儘管是單執行緒但已經足夠處理各類非同步任務。

當有計算密集型的耗時任務時,就需要建立新的Isolate來進行耗時計算來避免阻塞root isloate。由於不同Isolate之間記憶體隔離,要通訊就得通過 ReceivePort與SendPort 來實現。

使用Isolate.spawn來建立新的Isolate。看下函式簽名

  external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused = false,
      bool errorsAreFatal = true,
      SendPort? onExit,
      SendPort? onError,
      @Since("2.3") String? debugName});
複製程式碼

external修飾說明這個方法在不同的平臺有不同的實現,有點類似於java的native方法。

Future<Isolate>說明是非同步方法,要想同步程式碼中拿到建立好的Isolate,呼叫時得新增同步等待await。

entryPoint定義了所能接受的計算任務的函式簽名(空返回值,有且只有一個入參),必須是頂層函式或者靜態方法。

T message是在新Isolate中執行計算任務所需的引數,型別必須和entryPoint所能接受的型別一致。

所以建立新的Isolate最主要的就以下兩步:

  1. 使用頂層函式或靜態方法定義計算任務

    // 耗時計算部分
    int fibonacci(int n) {
      return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
    }
    // 1. 計算任務
    void task1(int start) {
      DateTime startTime = DateTime.now();
      int result = fibonacci(start);
      DateTime endTime = DateTime.now();
      print("計算耗時:${endTime.difference(startTime)}  結果:${result.toString()}");
    }
    void main() {
      task1(50);
    }
    // 輸出:計算耗時:0:00:48.608656  結果:12586269025
    複製程式碼

上面的計算要耗時48秒,必須要在新的Isolate中做計算

  1. 準備入參,呼叫spawn建立新的Isolate。

    void main() async {
      Isolate newIsolate = await Isolate.spawn(task1,10,debugName: "isolateDebug");
      print("結束!");
    }
    複製程式碼

    輸出結果:

    結束!
    複製程式碼

    在新的Isolate中計算得到的結果沒有列印到當前Console中,此時就要使用ReceivePort與SendPort來建立當前Isolate和新Isolate的連線。

單向通訊

定義可以接收宿主的SendPort的耗時任務,在計算完成後通過send方法將結果發回宿主Isolate。

/// [hostSendPort] 用於isolate向宿主isolate傳送結果
void task2(SendPort hostSendPort){
      DateTime startTime = DateTime.now();
      int result = fibonacci(47);
      DateTime endTime = DateTime.now();
      var state = "計算耗時:${endTime.difference(startTime)}  結果:${result
          .toString()}";
      hostSendPort.send(state);
}
複製程式碼

在宿主Isolate中要定義用於接收結果的ReceivePort,ReceivePort也是Stream,可以設定監聽來處理收到的結果。

void main() async {
  // 定義宿主接受結果的ReceivePort,設定監聽。
  ReceivePort hostReceivePort = ReceivePort();
  hostReceivePort.listen((message) {
    print(message);
  });
  // 定義向hostReceivePort 傳送資料的hostSendPort
  SendPort hostSendPort = hostReceivePort.sendPort;
  // hostSendPort.send("message"); 測試了在當前isolate中傳送結果的場景。
  Isolate newIsolate =
      await Isolate.spawn(task2, hostSendPort);
}
複製程式碼

計算結果:

計算耗時:0:00:11.959955  結果:2971215073
複製程式碼

雙向通訊

通過上面的改造,將宿主中的SendPort傳遞給子Isolate,在計算出結果後發回給宿主Isolate,宿主Isolate通過ReceivePort設定監聽來處理結果。就完成了子Isolate向宿主Isolate傳送資料。

但宿主Isolate如何向子Isolate傳送資料呢?

子Isolate向宿主Isolate傳送資料是通過,持有宿主中的SendPort來實現的。那麼要實現宿主Isolate向子Isolate傳送資料,宿主中也得持有子Isolate中的SendPort才行。最簡單的方案就是在子Isolate中建立subSendPort並傳遞迴宿主。

void task3(SendPort hostSendPort) {
  ///5.建立子Isolate自己的ReceivePort,用於接收宿主傳過來的初始化引數
  ReceivePort subReceivePort = ReceivePort();
  subReceivePort.listen((start) {
    if (start is int) {
      ///9. 收到宿主中初始化引數後進行計算。
      DateTime startTime = DateTime.now();
      int result = fibonacci(start);
      DateTime endTime = DateTime.now();
      var state =
          "計算耗時:${endTime.difference(startTime)}  結果:${result.toString()}";

      ///10.計算結束後通過宿主的hostSendPort將結果發出去。
      hostSendPort.send(state);
    }
  });

  ///6.將子Isolate自身的sendPort發給宿主,用於宿主向子Isolate傳遞初始化引數。
  hostSendPort.send(subReceivePort.sendPort);
}

void main() async {
  ///1 定義宿主接受結果的port和傳送引數的port
  ReceivePort hostReceivePort = ReceivePort();

  ///2 定義子Isolate的SendPort引用
  SendPort subSendPort;

  ///3 定義監聽,監聽的部分暫時不會執行
  hostReceivePort.listen((message) {
    if (message is SendPort) {
      /// 7.收到子Isolate的SendPort,此時完成了雙向通訊的配置階段
      subSendPort = message;

      /// 8.向子Isolate傳送計算任務的初始化資料
      subSendPort.send(2);
      subSendPort.send(10);
      subSendPort.send(20);
      subSendPort.send(30);
      subSendPort.send(40);
      subSendPort.send(48);
    } else if (message is String) {
      /// 11.列印子Isolate中計算的結果。
      print(message);
    } else {
      print("收到的資料不符合規範");
    }
  });

  ///4 定義向hostReceivePort 傳送資料的hostSendPort,開始建立
  SendPort hostSendPort = hostReceivePort.sendPort;
  Isolate newIsolate = await Isolate.spawn(task3, hostSendPort);
}
複製程式碼

最終輸出結果為:

計算耗時:0:00:00.000000  結果:1
計算耗時:0:00:00.000000  結果:55
計算耗時:0:00:00.000000  結果:6765
計算耗時:0:00:00.002999  結果:832040
計算耗時:0:00:00.393017  結果:102334155
計算耗時:0:00:19.179601  結果:4807526976
複製程式碼

上面的程式碼按順序看可能有點繞,新增了註釋描述了基本的執行順序。可以看出要完成一個基本的雙向通訊功能,要寫大段的配置的程式碼,真正屬於業務部分的程式碼就幾行,用起來很繁瑣。

簡化使用

Flutter中對Isolate的使用進行了簡化,通過compute方法可以很方便的實現雙向通訊。

import 'package:flutter/foundation.dart';
int fibonacci(int n) {
  return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}
void main()async{
    var result = await compute( fibonacci,20);
    print("計算結果為:$result");
}
複製程式碼

輸出結果:

I/flutter ( 9899): 計算結果為:6765
複製程式碼

引用:

blog.csdn.net/weixin_3387…

sg.jianshu.io/p/a4a871995…

相關文章