Flutter非同步與執行緒詳解

zxRisingSun 發表於 2022-05-09
Flutter

 

一:前言 - 關於多執行緒與非同步


 

      關於 Dart,我相信大家都知道Dart是一門單執行緒語言,這裡說的單執行緒並不是說Dart沒有或著不能使用多執行緒,而是Dart的所有API預設情況下都是單執行緒的。但大家也都知道Dart是有辦法支援多執行緒和非同步操作的,關於多執行緒和非同步這兩個概念是需要我們理清楚的,不能混淆它們的概念,給我們的理解造成困擾。

      1、多執行緒是開闢另外一個執行緒來處理事件,每個執行緒都有單獨的事件佇列,互不影響,這個新執行緒和當前執行緒是並列執行的,有的共享資料空間有的不共享(比如Isolate)。

      2、非同步是不阻塞當前執行緒,將非同步任務和當前執行緒的任務分開,非同步任務後面的任務,不會等待非同步任務執行完再執行,而是直接執行,與非同步任務的回撥沒有關係,這樣就不影響當前執行緒的執行,這就叫非同步。

      接下來我們按照 事件佇列 -- 非同步 -- 多執行緒 這樣的順序整理我們這篇的內容。

 

二:事件佇列


 

      這個和iOS比較類似,在Dart的執行緒中也存在事件迴圈和訊息佇列的概念,在Dart的執行緒中包含一個事件迴圈以及兩個事件佇列,我們先說清楚兩個事件佇列,再來整理它的事件迴圈或著說是訊息迴圈機制是什麼樣子的。

      1、事件任務佇列(Event Queue):負責處理I/O事件、繪製事件、手勢事件、接收其他Isolate訊息等外部事件,Timer也是事件佇列。

      2、微任務佇列(Microtask Queue)表示一個短時間內就會完成的非同步任務。它的優先順序最高,高於Event Queue,只要佇列中還有任務,就可以一直霸佔著事件迴圈。Microtask Queue新增的任務主要是由Dart內部產生,當然我們也可以自己新增任務到微任務佇列中去,但是我們不要在Microtask Queue裡面實現耗時操作避免阻塞Event Queue裡的UI事件導致卡頓現象。因為微任務佇列的優先順序要比事件佇列的高,所以事件迴圈每次迴圈總是先判斷微任務佇列中是否有任務需要執行,如果有則先執行微任務佇列裡的任務,執行完畢之後才會執行事件任務佇列裡的任務,就會造成卡頓。

      具體到兩個佇列的任務怎麼建立新增我們後面再提,在瞭解了這兩個佇列之後我們再看看Dart的訊息迴圈機制,下面這張圖相信大家都見到過:
 
Flutter非同步與執行緒詳解

      關於事件迴圈的,需要我們特別留意的:

      1、在Microtask不為空的時候,Run next Microtask 之後回到最開始,首先判斷的是是否還存在微任務,有的話還是優先處理的。

      2、在Event不為空的時候,Run next event之後,還是會回去判斷是否有Microtask,這點就把前面優先順序的問題說的很明白了,這兩點需要我們特別留意,在下面我們說完這兩個對壘任務的新增之後,我們會寫一個稍微比較複雜的方法,仔細的分析一下上面這個事件迴圈機制。

 

三:非同步


 

       在非同步呼叫中有三個關鍵詞 【async】【await】【Future】,其中async和await/Future是一起使用的,在Dart中可以通過async和await進行一個非同步操作,async表示開始一個非同步操作,也可以返回一個Future結果。如果沒有返回值,則預設返回一個返回值為null的Future,這點也比較容易理解,就像下面的方法,返回值是Future,而我們不寫返回return也是可以編譯過去的,就是它預設自己返回一個返回值為null的Future。

Future handleMessage(String message) async {
    print(message);
}

       Future:預設的Future是非同步執行的,也就是把任務放在Future函式體中,這個函式題會被非同步執行。

       async:非同步函式標識,一般與await和Future配合使用。

       await:等待非同步結果返回,一般加在Future函式體之前,表明後面的程式碼要等這個Future函式體內的內容執行完在執行,實現同步執行。單獨給函式新增async關鍵字, 沒有意義,函式是否是非同步的,主要看Future。

        注意:Future<T>通過泛型指定型別的非同步操作結果(不需要結果可以使用Future<void>)當一個返回Future物件的函式被呼叫時,函式將被放入佇列等待執行並返回一個未完成的Future物件,等函式操作執行完成時,Future物件變為完成並攜帶一個值或一個錯誤。也就是說首先Future是個泛型類,可以指定型別。如果沒有指定相應型別的話,則Future會在執行動態的推導型別。

       結合上面說的這幾點,我們寫個實際的小例子:
 
class asyncIsolate {

  Future<HttpClientRequest> dataReqeust() async {

    var httpClient = new HttpClient();
    /// var uri = Uri.https('example.org', '/path', {'q': 'dart'});
    /// print(uri); // https://example.org/path?q=dart
    Future<HttpClientRequest> request =
        httpClient.getUrl(Uri.https('jsonplaceholder.typicode.com', '/posts'));
    return request;
  }

  Future<String> loadData() async {

    HttpClientRequest request = await dataReqeust();
    var response = await request.close();
    var responseBody = await response.transform(Utf8Decoder()).join();
    print('請求到的資料為:\n $responseBody');
    return responseBody;
  }

}

      上面的方法是一個請求資料的小demo,我們呼叫loadData方法進行資料請求,在執行到loadData內部時候,執行到await會阻塞async內部的執行,從而繼續執行外面的程式碼,一直到dataReqeust的方法有返回,再接著async內部的執行,所以需要知道的事await不會阻塞方法外部程式碼的執行。

       Future可以看做是一個延遲操作的封裝,可以將非同步任務封裝為Future物件。獲取到Future物件後,最簡單的方法就是用await修飾,並等待返回結果繼續向下執行。在Dart中,和時間相關的操作基本都和Future有關,例如延時操作、非同步操作等,下面是一個最簡單的延遲操作的例子:

/// 延遲操作
delayedWithFuture() {

    DateTime now = DateTime.now();
    print("開始時間: $now");
    Future.delayed(Duration(seconds: 10), () {
      now = DateTime.now();
      print("延遲10秒後的時間: $now");
    });

    /*
    flutter: 開始時間: 2022-05-09 13:30:07.164114
    flutter: 延遲10秒後的時間: 2022-05-09 13:30:17.171057
    */
}

      Dart還支援對Future的鏈式呼叫,通過追加一個或多個then方法來實現,這個特性非常實用。例如一個延時操作完成後,會呼叫then方法,並且可以傳遞一個引數給then,比如下面的例子:

delayWithFutureThen() {

    Future.delayed(Duration(seconds: 5), () {
      int age = 30;
      return age;
    }).then((onValue) {
      onValue++;
      print('我多大了啊 $onValue');
    });

    /*
    flutter: 我多大了啊 31
    */
}

      Future還有很多有意思的方法,比如 Future.doWhile() 、Future.any()、Future.wait(),我們簡單的看一個,比如Future.wait的用法,假設我們有這個一個使用場景,等待在三個Future執行完之後我們還需要執行另外一個Future,這時候我們該怎麼處理,下面的demo給了我們處理的方式,注意下輸出的日誌,我們第一個是延遲的Future,是延遲兩秒後輸出的。

///
void awaitWithFuture() async {

    Future future1 = Future.delayed(Duration(seconds: 2), () {
      print(1);
      return 1;
    });

    Future future2 = Future(() {
      print(2);
      return 2;
    });

    Future future3 = Future(() {
      print(3);
      return 3;
    });

    Future.wait([future1, future2, future3]).then((value) {
      print(value);
    }).catchError((error) {});

    /*
    flutter: 2
    flutter: 3
    flutter: 1
    flutter: [1, 2, 3]
    */
}

      微任務佇列新增任務,我們通過scheduleMicrotask新增微任務,具體的我們就不在這再寫了,的確新增比較簡單,接下來我們寫一個事件佇列和微任務佇列在一起的demo,我們梳理一下執行的過程,加深一下對事件迴圈的理解:

  analyseWithAsyncTask() {

    print('foundation start');
    Future.delayed(Duration(seconds: 2), (() {
      print('Future - delayed 2 second'); // --- 6
    }));

    Future(() {
      print('Future - 1'); // --- 2
    });

    Future(() {
      print('Future - 2'); // --- 3
    });

    Timer(Duration(seconds: 3), (() {
      print('Timer - delayed 3 second'); // --- 7
    }));

    scheduleMicrotask((() {
      print("Microtask - 1"); // --- 1
      Future(() {
        print('Future - 3'); // --- 5 【4的Future新增的比Future - 3要早】
      });
    }));

    Future(() {
      scheduleMicrotask((() {
        print("Microtask - 2"); // --- 4
      }));
    });

    print('foundation end');
  }

  /*
  按照自己理解寫出來的執行順序

  先到foundation start 再執行到
  Future.delayed(Duration(seconds: 2), (() {
    print('Future - delayed 2 second');  // --- 6
  }));
  沒有這個Future新增到事件佇列、後面的
  Future(() {
    print('Future - 2'); // --- 3
  });

  Timer(Duration(seconds: 3), (() {
    print('Timer - delayed 3 second'); // --- 7
  }));
  也一樣,沒有先新增到事件佇列、接下來是
  scheduleMicrotask((() {
    print("Microtask - 1"); // --- 1
    Future(() {
      print('Future - 3'); // --- 5 【4的Future新增的比Future - 3要早】
    });
  }));
  判斷微任務佇列沒有這個任務,新增到微任務佇列。再到後面的
  Future(() {
    scheduleMicrotask((() {
      print("Microtask - 2"); // --- 4
    }));
  });
  也一樣,也是沒有就新增到事件佇列,接著就是先列印foundation end
  接下來判斷有沒有優先順序更搞得微任務佇列是否為空,判斷有任務不為空,則執行微任務輸出 - Microtask - 1 ,繼續執行判斷沒有事件任務Future - 3
  把事件任務新增到事件佇列,注意這個事件任務的位置是在標記了// --- 4的事件後面的,執行完判斷有沒有微任務,發現沒有了,開始新增的順序執行事件任務
  就輸出了Future - 1 Future - 2 ,執行// --- 4的時候發現微任務,新增到微任務佇列,執行下一個事件任務之前,判斷有沒有微任務,有的話就去執行微任務
  就執行了Microtask - 2 ,繼續判斷微任務空了,繼續事件任務。就到了Future - 3 最後兩個延時的,安演示正長短 短的先執行

  foundation start
  foundation end
  Microtask - 1
  Future - 1
  Future - 2
  Microtask - 2
  Future - 3
  Future - delayed 2 second
  Timer - delayed 3 second

  實際日誌輸出:
  flutter: foundation start
  flutter: foundation end
  flutter: Microtask - 1
  flutter: Future - 1
  flutter: Future - 2
  flutter: Microtask - 2
  flutter: Future - 3
  flutter: Future - delayed 2 second
  flutter: Timer - delayed 3 second
  */

 

四: 多執行緒 - Isolate


 

       Isolate是Dart平臺對執行緒的實現方案,但和普通Thread不同的是,isolate擁有獨立的記憶體,isolate由執行緒和獨立記憶體構成。正是由於isolate執行緒之間的記憶體不共享,所以isolate執行緒之間並不存在資源搶奪的問題,所以也不需要鎖。
 
      通過isolate可以很好的利用多核CPU,來進行大量耗時任務的處理。isolate執行緒之間的通訊主要通過Port來進行,這個Port訊息傳遞的過程是非同步的。通過Dart原始碼也可以看出,例項化一個isolate的過程包括,例項化isolate結構體、在堆中分配執行緒記憶體、配置Port等過程。
 
  late SendPort subSendPort;

  /// 建立新的執行緒
  dispatchQueueAsyncThread() async {
    /// 主執行緒埠
    ReceivePort mainThreadPort = ReceivePort();

    /// 建立一個新的執行緒
    /*
      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});   |
    */

    Isolate isolate =
        await Isolate.spawn<SendPort>(dataLoader, mainThreadPort.sendPort);
    mainThreadPort.listen((message) {
      ///
      print(message);

      if (message is SendPort) {
        subSendPort = message;
        print("子執行緒建立成功");
        print("主執行緒收到了子執行緒的ReceivePort的Sendport了,可以通訊了");
      } else if (message is String) {
        if (message == "closed") {
          /// 結束這個執行緒
          print('isolate kill');
          isolate.kill();
        }
      }
    });
  }

  /// 主執行緒傳送訊息給子執行緒
  mainSendMessageToSubThread() {
    if (subSendPort != null) {
      subSendPort.send("我是你的主執行緒");
    }
  }

  /// 主執行緒傳送關閉埠的訊息給子執行緒
  /// 子執行緒關閉介面埠 並告訴主執行緒 主執行緒結束子執行緒
  mainSendClosedThreadMessageToSubThread() {
    if (subSendPort != null) {
      subSendPort.send("close receiveport");
    }
  }

  // 這個SendPort是前面主執行緒的
  static dataLoader(SendPort sendPort) async {
    /// 子執行緒的ReceivePort建立
    ReceivePort subThreadPort = ReceivePort();

    /// 這是把子執行緒的ReceivePort的sendPort給了主執行緒 用於通訊
    sendPort.send(subThreadPort.sendPort);

    subThreadPort.listen((message) {
      print("子執行緒收到的訊息 $message");
      if (message is String) {
        /// 收到主執行緒讓關閉介面的訊息 就關閉 正確的話後面是在接受不到訊息了
        if (message == "close receiveport") {
          sendPort.send('closed');
          subThreadPort.close();
        }
      }
    });
  }

  /*
  flutter: SendPort
  flutter: 子執行緒建立成功
  flutter: 主執行緒收到了子執行緒的ReceivePort的Sendport了,可以通訊了
  flutter: 子執行緒收到的訊息 我是你的主執行緒
  flutter: 子執行緒收到的訊息 close receiveport
  flutter: closed
  flutter: isolate kill
  */

      Isolate的執行緒更加偏向於底層,在生成一個Isolate之後,其記憶體是各自獨立的,相互之間並不能進行訪問,在進行Isolate訊息傳遞的過程中,本質上就是進行Port的傳遞,通過上面的小例子我們基本上也就掌握了最基礎的Flutter訊息執行緒建立和執行緒之間的訊息傳遞。