Flutter(五)之徹底搞懂Dart非同步

coderwhy發表於2019-09-16

前言一:接下來一段時間我會陸續更新一些列Flutter文字教程

更新進度: 每週至少兩篇;

更新地點: 首發於公眾號,第二天更新於掘金、思否等地方;

更多交流: 可以新增我的微信 372623326,關注我的微博:coderwhy

希望大家可以 幫忙轉發,點選在看,給我更多的創作動力。

前言二:在寫這篇文章之前,我一直在猶豫,要不要在這裡講解Dart的非同步相關話題,因為這部分內容很容易讓初學者望而卻步:

1、關於單執行緒和非同步之間的關係,比較容易讓人迷惑,雖然我一定會用自己的方式儘可能讓你聽懂。

2、大量的非同步操作方式(Future、await、async等),目前你看不到具體的應用場景。(比如你學習過前端中的Promise、await、async可能會比較簡單,但是我會假設你沒有這樣的基礎)。

不過,聽我說:如果這一個章節你學完之後還有很多疑惑,沒有關係,在後面用到相關知識時,回頭來看,你會豁然開朗。

一. Dart的非同步模型

我們先來搞清楚Dart是如何搞定非同步操作的

1.1. Dart是單執行緒的

1.1.1. 程式中的耗時操作

開發中的耗時操作:

  • 在開發中,我們經常會遇到一些耗時的操作需要完成,比如網路請求、檔案讀取等等;
  • 如果我們的主執行緒一直在等待這些耗時的操作完成,那麼就會進行阻塞,無法響應其它事件,比如使用者的點選;
  • 顯然,我們不能這麼幹!!

如何處理耗時的操作呢?

  • 針對如何處理耗時的操作,不同的語言有不同的處理方式。
  • 處理方式一: 多執行緒,比如Java、C++,我們普遍的做法是開啟一個新的執行緒(Thread),在新的執行緒中完成這些非同步的操作,再通過執行緒間通訊的方式,將拿到的資料傳遞給主執行緒。
  • 處理方式二: 單執行緒+事件迴圈,比如JavaScript、Dart都是基於單執行緒加事件迴圈來完成耗時操作的處理。不過單執行緒如何能進行耗時的操作呢?!

1.1.2. 單執行緒的非同步操作

我之前碰到很多開發者都對單執行緒的非同步操作充滿了問號???

單執行緒非同步操作

其實它們並不衝突:

  • 因為我們的一個應用程式大部分時間都是處於空閒的狀態的,並不是無限制的在和使用者進行互動。
  • 比如等待使用者點選、網路請求資料的返回、檔案讀寫的IO操作,這些等待的行為並不會阻塞我們的執行緒;
  • 這是因為類似於網路請求、檔案讀寫的IO,我們都可以基於非阻塞呼叫;

阻塞式呼叫和非阻塞式呼叫

如果想搞懂這個點,我們需要知道作業系統中的阻塞式呼叫非阻塞式呼叫的概念。

  • 阻塞和非阻塞關注的是程式在等待呼叫結果(訊息,返回值)時的狀態。
  • 阻塞式呼叫: 呼叫結果返回之前,當前執行緒會被掛起,呼叫執行緒只有在得到呼叫結果之後才會繼續執行。
  • 非阻塞式呼叫: 呼叫執行之後,當前執行緒不會停止執行,只需要過一段時間來檢查一下有沒有結果返回即可。

我們用一個生活中的例子來模擬:

  • 你中午餓了,需要點一份外賣,點外賣的動作就是我們的呼叫,拿到最後點的外賣就是我們要等待的結果。
  • 阻塞式呼叫: 點了外賣,不再做任何事情,就是在傻傻的等待,你的執行緒停止了任何其他的工作。
  • 非阻塞式呼叫: 點了外賣,繼續做其他事情:繼續工作、打把遊戲,你的執行緒沒有繼續執行其他事情,只需要偶爾去看一下有沒有人敲門,外賣有沒有送到即可。

而我們開發中的很多耗時操作,都可以基於這樣的 非阻塞式呼叫

  • 比如網路請求本身使用了Socket通訊,而Socket本身提供了select模型,可以進行非阻塞方式的工作
  • 比如檔案讀寫的IO操作,我們可以使用作業系統提供的基於事件的回撥機制

這些操作都不會阻塞我們單執行緒的繼續執行,我們的執行緒在等待的過程中可以繼續去做別的事情:喝杯咖啡、打把遊戲,等真正有了響應,再去進行對應的處理即可。

這時,我們可能有兩個問題:

  • 問題一: 如果在多核CPU中,單執行緒是不是就沒有充分利用CPU呢?這個問題,我會放在後面來講解。
  • 問題二: 單執行緒是如何來處理網路通訊、IO操作它們返回的結果呢?答案就是事件迴圈(Event Loop)。

1.2. Dart事件迴圈

1.2.1. 什麼是事件迴圈

單執行緒模型中主要就是在維護著一個事件迴圈(Event Loop)。

事件迴圈是什麼呢?

  • 事實上事件迴圈並不複雜,它就是將需要處理的一系列事件(包括點選事件、IO事件、網路事件)放在一個事件佇列(Event Queue)中。
  • 不斷的從事件佇列(Event Queue)中取出事件,並執行其對應需要執行的程式碼塊,直到事件佇列清空位置。

我們來寫一個事件迴圈的虛擬碼:

// 這裡我使用陣列模擬佇列, 先進先出的原則
List eventQueue = []; 
var event;

// 事件迴圈從啟動的一刻,永遠在執行
while (true) {
  if (eventQueue.length > 0) {
    // 取出一個事件
    event = eventQueue.removeAt(0);
    // 執行該事件
    event();
  }
}
複製程式碼

當我們有一些事件時,比如點選事件、IO事件、網路事件時,它們就會被加入到eventLoop中,當發現事件佇列不為空時發現,就會取出事件,並且執行。

  • 齒輪就是我們的事件迴圈,它會從佇列中一次取出事件來執行。

img

1.2.2. 事件迴圈程式碼模擬

這裡我們來看一段虛擬碼,理解點選事件和網路請求的事件是如何被執行的:

  • 這是一段Flutter程式碼,很多東西大家可能不是特別理解,但是耐心閱讀你會讀懂我們在做什麼。
  • 一個按鈕RaisedButton,當發生點選時執行onPressed函式。
  • onPressed函式中,我們傳送了一個網路請求,請求成功後會執行then中的回撥函式。
RaisedButton(
  child: Text('Click me'),
  onPressed: () {
    final myFuture = http.get('https://example.com');
    myFuture.then((response) {
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)
複製程式碼

這些程式碼是如何放在事件迴圈中執行呢?

  • 1、當使用者發生點選的時候,onPressed回撥函式被放入事件迴圈中執行,執行的過程中傳送了一個網路請求。
  • 2、網路請求發出去後,該事件迴圈不會被阻塞,而是發現要執行的onPressed函式已經結束,會將它丟棄掉。
  • 3、網路請求成功後,會執行then中傳入的回撥函式,這也是一個事件,該事件被放入到事件迴圈中執行,執行完畢後,事件迴圈將其丟棄。

儘管onPressed和then中的回撥有一些差異,但是它們對於事件迴圈來說,都是告訴它:我有一段程式碼需要執行,快點幫我完成。

二. Dart的非同步操作

Dart中的非同步操作主要使用Future以及async、await。

如果你之前有過前端的ES6、ES7程式設計經驗,那麼完全可以將Future理解成Promise,async、await和ES7中基本一致。

但是如果沒有前端開發經驗,Future以及async、await如何理解呢?

2.1. 認識Future

我思考了很久,這個Future到底應該如何講解

2.1.1. 同步的網路請求

我們先來看一個例子吧:

  • 在這個例子中,我使用getNetworkData來模擬了一個網路請求;
  • 該網路請求需要3秒鐘的時間,之後返回資料;
import "dart:io";

main(List<String> args) {
  print("main function start");
  print(getNetworkData());
  print("main function end");
}

String getNetworkData() {
  sleep(Duration(seconds: 3));
  return "network data";
}
複製程式碼

這段程式碼會執行怎麼的結果呢?

  • getNetworkData會阻塞main函式的執行
main function start
// 等待3秒
network data
main function end
複製程式碼

顯然,上面的程式碼不是我們想要的執行效果,因為網路請求阻塞了main函式,那麼意味著其後所有的程式碼都無法正常的繼續執行。

2.1.2. 非同步的網路請求

我們來對我們上面的程式碼進行改進,程式碼如下:

  • 和剛才的程式碼唯一的區別在於我使用了Future物件來將耗時的操作放在了其中傳入的函式中;
  • 稍後,我們會講解它具體的一些API,我們就暫時知道我建立了一個Future例項即可;
import "dart:io";

main(List<String> args) {
  print("main function start");
  print(getNetworkData());
  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    return "network data";
  });
}
複製程式碼

我們來看一下程式碼的執行結果:

  • 1、這一次的程式碼順序執行,沒有出現任何的阻塞現象;
  • 2、和之前直接列印結果不同,這次我們列印了一個Future例項;
  • 結論:我們將一個耗時的操作隔離了起來,這個操作不會再影響我們的主執行緒執行了。
  • 問題:我們如何去拿到最終的結果呢?
main function start
Instance of 'Future<String>'
main function end
複製程式碼

獲取Future得到的結果

有了Future之後,如何去獲取請求到的結果:通過.then的回撥:

main(List<String> args) {
  print("main function start");
  // 使用變數接收getNetworkData返回的future
  var future = getNetworkData();
  // 當future例項有返回結果時,會自動回撥then中傳入的函式
  // 該函式會被放入到事件迴圈中,被執行
  future.then((value) {
    print(value);
  });
  print(future);
  print("main function end");
}
複製程式碼

上面程式碼的執行結果:

main function start
Instance of 'Future<String>'
main function end
// 3s後執行下面的程式碼
network data
複製程式碼

執行中出現異常

如果呼叫過程中出現了異常,拿不到結果,如何獲取到異常的資訊呢?

import "dart:io";

main(List<String> args) {
  print("main function start");
  var future = getNetworkData();
  future.then((value) {
    print(value);
  }).catchError((error) { // 捕獲出現異常時的情況
    print(error);
  });
  print(future);
  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    // 不再返回結果,而是出現異常
    // return "network data";
    throw Exception("網路請求出現錯誤");
  });
}

複製程式碼

上面程式碼的執行結果:

main function start
Instance of 'Future<String>'
main function end
// 3s後沒有拿到結果,但是我們捕獲到了異常
Exception: 網路請求出現錯誤

複製程式碼

2.1.3. Future使用補充

補充一:上面案例的小結

我們通過一個案例來學習了一些Future的使用過程:

  • 1、建立一個Future(可能是我們建立的,也可能是呼叫內部API或者第三方API獲取到的一個Future,總之你需要獲取到一個Future例項,Future通常會對一些非同步的操作進行封裝);
  • 2、通過.then(成功回撥函式)的方式來監聽Future內部執行完成時獲取到的結果;
  • 3、通過.catchError(失敗或異常回撥函式)的方式來監聽Future內部執行失敗或者出現異常時的錯誤資訊;

補充二:Future的兩種狀態

事實上Future在執行的整個過程中,我們通常把它劃分成了兩種狀態:

狀態一:未完成狀態(uncompleted)

  • 執行Future內部的操作時(在上面的案例中就是具體的網路請求過程,我們使用了延遲來模擬),我們稱這個過程為未完成狀態

狀態二:完成狀態(completed)

  • 當Future內部的操作執行完成,通常會返回一個值,或者丟擲一個異常。
  • 這兩種情況,我們都稱Future為完成狀態。

Dart官網有對這兩種狀態解析,之所以貼出來是區別於Promise的三種狀態

dart官網

補充三:Future的鏈式呼叫

上面程式碼我們可以進行如下的改進:

  • 我們可以在then中繼續返回值,會在下一個鏈式的then呼叫回撥函式中拿到返回的結果
import "dart:io";

main(List<String> args) {
  print("main function start");

  getNetworkData().then((value1) {
    print(value1);
    return "content data2";
  }).then((value2) {
    print(value2);
    return "message data3";
  }).then((value3) {
    print(value3);
  });

  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    // 不再返回結果,而是出現異常
     return "network data1";
  });
}

複製程式碼

列印結果如下:

main function start
main function end
// 3s後拿到結果
network data1
content data2
message data3

複製程式碼

補充四:Future其他API

Future.value(value)

  • 直接獲取一個完成的Future,該Future會直接呼叫then的回撥函式
main(List<String> args) {
  print("main function start");

  Future.value("哈哈哈").then((value) {
    print(value);
  });

  print("main function end");
}

複製程式碼

列印結果如下:

main function start
main function end
哈哈哈

複製程式碼

疑惑:為什麼立即執行,但是哈哈哈是在最後列印的呢?

  • 這是因為Future中的then會作為新的任務會加入到事件佇列中(Event Queue),加入之後你肯定需要排隊執行了

Future.error(object)

  • 直接獲取一個完成的Future,但是是一個發生異常的Future,該Future會直接呼叫catchError的回撥函式
main(List<String> args) {
  print("main function start");

  Future.error(Exception("錯誤資訊")).catchError((error) {
    print(error);
  });

  print("main function end");
}

複製程式碼

列印結果如下:

main function start
main function end
Exception: 錯誤資訊

複製程式碼

Future.delayed(時間, 回撥函式)

  • 在延遲一定時間時執行回撥函式,執行完回撥函式後會執行then的回撥;
  • 之前的案例,我們也可以使用它來模擬,但是直接學習這個API會讓大家更加疑惑;
main(List<String> args) {
  print("main function start");

  Future.delayed(Duration(seconds: 3), () {
    return "3秒後的資訊";
  }).then((value) {
    print(value);
  });

  print("main function end");
}

複製程式碼

2.2. await、async

2.2.1. 理論概念理解

如果你已經完全搞懂了Future,那麼學習await、async應該沒有什麼難度。

await、async是什麼呢?

  • 它們是Dart中的關鍵字(你這不是廢話嗎?廢話也還是要強調的,萬一你用它做變數名呢,無辜臉。)
  • 它們可以讓我們用同步的程式碼格式,去實現非同步的呼叫過程
  • 並且,通常一個async的函式會返回一個Future(彆著急,馬上就看到程式碼了)。

我們已經知道,Future可以做到不阻塞我們的執行緒,讓執行緒繼續執行,並且在完成某個操作時改變自己的狀態,並且回撥then或者errorCatch回撥。

如何生成一個Future呢?

  • 1、通過我們前面學習的Future建構函式,或者後面學習的Future其他API都可以。
  • 2、還有一種就是通過async的函式。

2.2.2. 案例程式碼演練

Talk is cheap. Show me the code.

我們來對之前的Future非同步處理程式碼進行改造,改成await、async的形式。

我們知道,如果直接這樣寫程式碼,程式碼是不能正常執行的:

  • 因為Future.delayed返回的是一個Future物件,我們不能把它看成同步的返回資料:"network data"去使用
  • 也就是我們不能把這個非同步的程式碼當做同步一樣去使用!
import "dart:io";

main(List<String> args) {
  print("main function start");
  print(getNetworkData());
  print("main function end");
}

String getNetworkData() {
  var result = Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return  "請求到的資料:" + result;
}

複製程式碼

現在我使用await修改下面這句程式碼:

  • 你會發現,我在Future.delayed函式前加了一個await。
  • 一旦有了這個關鍵字,那麼這個操作就會等待Future.delayed的執行完畢,並且等待它的結果。
String getNetworkData() {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return  "請求到的資料:" + result;
}

複製程式碼

修改後執行程式碼,會看到如下的錯誤:

  • 錯誤非常明顯:await關鍵字必須存在於async函式中。
  • 所以我們需要將getNetworkData函式定義成async函式。

image-20190913153558169

繼續修改程式碼如下:

  • 也非常簡單,只需要在函式的()後面加上一個async關鍵字就可以了
String getNetworkData() async {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return  "請求到的資料:" + result;
}

複製程式碼

執行程式碼,依然報錯(心想:你妹啊):

  • 錯誤非常明顯:使用async標記的函式,必須返回一個Future物件。
  • 所以我們需要繼續修改程式碼,將返回值寫成一個Future。

image-20190913153648117

繼續修改程式碼如下:

Future<String> getNetworkData() async {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return "請求到的資料:" + result;
}

複製程式碼

這段程式碼應該是我們理想當中執行的程式碼了

  • 我們現在可以像同步程式碼一樣去使用Future非同步返回的結果;
  • 等待拿到結果之後和其他資料進行拼接,然後一起返回;
  • 返回的時候並不需要包裝一個Future,直接返回即可,但是返回值會預設被包裝在一個Future中;

2.3. 讀取json案例

我這裡給出了一個在Flutter專案中,讀取一個本地的json檔案,並且轉換成模型物件,返回出去的案例;

這個案例作為大家學習前面Future和await、async的一個參考,我並不打算展開來講,因為需要用到Flutter的相關知識;

後面我會在後面的案例中再次講解它在Flutter中我使用的過程中;

讀取json案例程式碼(瞭解一下即可)

import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';

main(List<String> args) {
  getAnchors().then((anchors) {
    print(anchors);
  });
}

class Anchor {
  String nickname;
  String roomName;
  String imageUrl;

  Anchor({
    this.nickname,
    this.roomName,
    this.imageUrl
  });

  Anchor.withMap(Map<String, dynamic> parsedMap) {
    this.nickname = parsedMap["nickname"];
    this.roomName = parsedMap["roomName"];
    this.imageUrl = parsedMap["roomSrc"];
  }
}

Future<List<Anchor>> getAnchors() async {
  // 1.讀取json檔案
  String jsonString = await rootBundle.loadString("assets/yz.json");

  // 2.轉成List或Map型別
  final jsonResult = json.decode(jsonString);

  // 3.遍歷List,並且轉成Anchor物件放到另一個List中
  List<Anchor> anchors = new List();
  for (Map<String, dynamic> map in jsonResult) {
    anchors.add(Anchor.withMap(map));
  }
  return anchors;
}

複製程式碼

三. Dart的非同步補充

3.1. 任務執行順序

3.1.1. 認識微任務佇列

在前面學習學習中,我們知道Dart中有一個事件迴圈(Event Loop)來執行我們的程式碼,裡面存在一個事件佇列(Event Queue),事件迴圈不斷從事件佇列中取出事件執行。

但是如果我們嚴格來劃分的話,在Dart中還存在另一個佇列:微任務佇列(Microtask Queue)。

  • 微任務佇列的優先順序要高於事件佇列;
  • 也就是說事件迴圈都是優先執行微任務佇列中的任務,再執行 事件佇列 中的任務;

那麼在Flutter開發中,哪些是放在事件佇列,哪些是放在微任務佇列呢?

  • 所有的外部事件任務都在事件佇列中,如IO、計時器、點選、以及繪製事件等;
  • 而微任務通常來源於Dart內部,並且微任務非常少。這是因為如果微任務非常多,就會造成事件佇列排不上隊,會阻塞任務佇列的執行(比如使用者點選沒有反應的情況);

說道這裡,你可能已經有點凌亂了,在Dart的單執行緒中,程式碼到底是怎樣執行的呢?

  • 1、Dart的入口是main函式,所以main函式中的程式碼會優先執行;
  • 2、main函式執行完後,會啟動一個事件迴圈(Event Loop)就會啟動,啟動後開始執行佇列中的任務;
  • 3、首先,會按照先進先出的順序,執行 微任務佇列(Microtask Queue)中的所有任務;
  • 4、其次,會按照先進先出的順序,執行 事件佇列(Event Queue)中的所有任務;

程式碼執行順序

3.1.2. 如何建立微任務

在開發中,我們可以通過dart中async下的scheduleMicrotask來建立一個微任務:

import "dart:async";

main(List<String> args) {
  scheduleMicrotask(() {
    print("Hello Microtask");
  });
}
複製程式碼

在開發中,如果我們有一個任務不希望它放在Event Queue中依次排隊,那麼就可以建立一個微任務了。

Future的程式碼是加入到事件佇列還是微任務佇列呢?

Future中通常有兩個函式執行體:

  • Future建構函式傳入的函式體
  • then的函式體(catchError等同看待)

那麼它們是加入到什麼佇列中的呢?

  • Future建構函式傳入的函式體放在事件佇列中
  • then的函式體要分成三種情況:
  • 情況一:Future沒有執行完成(有任務需要執行),那麼then會直接被新增到Future的函式執行體後;
  • 情況二:如果Future執行完後就then,該then的函式體被放到如微任務佇列,當前Future執行完後執行微任務佇列;
  • 情況三:如果Future世鏈式呼叫,意味著then未執行完,下一個then不會執行;
// future_1加入到eventqueue中,緊隨其後then_1被加入到eventqueue中
Future(() => print("future_1")).then((_) => print("then_1"));

// Future沒有函式執行體,then_2被加入到microtaskqueue中
Future(() => null).then((_) => print("then_2"));

// future_3、then_3_a、then_3_b依次加入到eventqueue中
Future(() => print("future_3")).then((_) => print("then_3_a")).then((_) => print("then_3_b"));
複製程式碼

3.1.3. 程式碼執行順序

我們根據前面的規則來學習一個極的程式碼執行順序案例:

import "dart:async";

main(List<String> args) {
  print("main start");

  Future(() => print("task1"));
	
  final future = Future(() => null);

  Future(() => print("task2")).then((_) {
    print("task3");
    scheduleMicrotask(() => print('task4'));
  }).then((_) => print("task5"));

  future.then((_) => print("task6"));
  scheduleMicrotask(() => print('task7'));

  Future(() => print('task8'))
    .then((_) => Future(() => print('task9')))
    .then((_) => print('task10'));

  print("main end");
}

複製程式碼

程式碼執行的結果是:

main start
main end
task7
task1
task6
task2
task3
task5
task4
task8
task9
task10

複製程式碼

程式碼分析:

  • 1、main函式先執行,所以main startmain end先執行,沒有任何問題;
  • 2、main函式執行過程中,會將一些任務分別加入到EventQueueMicrotaskQueue中;
  • 3、task7通過scheduleMicrotask函式呼叫,所以它被最早加入到MicrotaskQueue,會被先執行;
  • 4、然後開始執行EventQueue,task1被新增到EventQueue中被執行;
  • 5、通過final future = Future(() => null);建立的future的then被新增到微任務中,微任務直接被優先執行,所以會執行task6;
  • 6、一次在EventQueue中新增task2、task3、task5被執行;
  • 7、task3的列印執行完後,呼叫scheduleMicrotask,那麼在執行完這次的EventQueue後會執行,所以在task5後執行task4(注意:scheduleMicrotask的呼叫是作為task3的一部分程式碼,所以task4是要在task5之後執行的)
  • 8、task8、task9、task10一次新增到EventQueue被執行;

事實上,上面的程式碼執行順序有可能出現在面試中,我們開發中通常不會出現這種複雜的巢狀,並且需要完全搞清楚它的執行順序;

但是,瞭解上面的程式碼執行順序,會讓你對EventQueuemicrotaskQueue有更加深刻的理解。

3.2. 多核CPU的利用

3.2.1. Isolate的理解

在Dart中,有一個Isolate的概念,它是什麼呢?

  • 我們已經知道Dart是單執行緒的,這個執行緒有自己可以訪問的記憶體空間以及需要執行的事件迴圈;
  • 我們可以將這個空間系統稱之為是一個Isolate;
  • 比如Flutter中就有一個Root Isolate,負責執行Flutter的程式碼,比如UI渲染、使用者互動等等;

在 Isolate 中,資源隔離做得非常好,每個 Isolate 都有自己的 Event Loop 與 Queue,

  • Isolate 之間不共享任何資源,只能依靠訊息機制通訊,因此也就沒有資源搶佔問題。

但是,如果只有一個Isolate,那麼意味著我們只能永遠利用一個執行緒,這對於多核CPU來說,是一種資源的浪費。

如果在開發中,我們有非常多耗時的計算,完全可以自己建立Isolate,在獨立的Isolate中完成想要的計算操作。

如何建立Isolate呢?

建立Isolate是比較簡單的,我們通過Isolate.spawn就可以建立了:

import "dart:isolate";

main(List<String> args) {
  Isolate.spawn(foo, "Hello Isolate");
}

void foo(info) {
  print("新的isolate:$info");
}

複製程式碼

3.2.2. Isolate通訊機制

但是在真實開發中,我們不會只是簡單的開啟一個新的Isolate,而不關心它的執行結果:

  • 我們需要新的Isolate進行計算,並且將計算結果告知Main Isolate(也就是預設開啟的Isolate);
  • Isolate 通過傳送管道(SendPort)實現訊息通訊機制;
  • 我們可以在啟動併發Isolate時將Main Isolate的傳送管道作為引數傳遞給它;
  • 併發在執行完畢時,可以利用這個管道給Main Isolate傳送訊息;
import "dart:isolate";

main(List<String> args) async {
  // 1.建立管道
  ReceivePort receivePort= ReceivePort();

  // 2.建立新的Isolate
  Isolate isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort);

  // 3.監聽管道訊息
  receivePort.listen((data) {
    print('Data:$data');
    // 不再使用時,我們會關閉管道
    receivePort.close();
    // 需要將isolate殺死
    isolate?.kill(priority: Isolate.immediate);
  });
}

void foo(SendPort sendPort) {
  sendPort.send("Hello World");
}

複製程式碼

但是我們上面的通訊變成了單向通訊,如果需要雙向通訊呢?

  • 事實上雙向通訊的程式碼會比較麻煩;
  • Flutter提供了支援併發計算的compute函式,它內部封裝了Isolate的建立和雙向通訊;
  • 利用它我們可以充分利用多核心CPU,並且使用起來也非常簡單;

注意:下面的程式碼不是dart的API,而是Flutter的API,所以只有在Flutter專案中才能執行

main(List<String> args) async {
  int result = await compute(powerNum, 5);
  print(result);
}

int powerNum(int num) {
  return num * num;
}

複製程式碼

備註:所有內容首發於公眾號,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注

公眾號

相關文章