[譯] Flutter 異步編程:Future、Isolate 和事件循環

本文介紹了 Flutter 中不一樣的代碼執行模式:單線程、多線程、同步和異步。html

難度:中級前端

概要

我最近收到了一些與 FutureasyncawaitIsolate 以及並行執行概念相關的一些問題。android

因爲這些問題,一些人在處理代碼的執行順序方面遇到了麻煩。ios

我認爲經過一篇文章來解釋異步並行處理這些概念並消除其中任何歧義是很是有用的。git


Dart 是一種單線程語言

首先,你們須要牢記,Dart單線程的而且 Flutter 依賴於 Dartgithub

重點編程

Dart 同一時刻只執行一個操做,其餘操做在該操做以後執行,這意味着只要一個操做正在執行,它就不會被其餘 Dart 代碼中斷。後端

也就是說,若是你考慮純粹的同步方法,那麼在它完成以前,後者將是惟一要執行的方法。api

void myBigLoop(){
    for (int i = 0; i < 1000000; i++){
        _doSomethingSynchronously();
    }
}
複製代碼

在上面的例子中,myBigLoop() 方法在執行完成前永遠不會被中斷。所以,若是該方法須要一些時間,那麼在整個方法執行期間應用將會被阻塞數組


Dart 執行模型

那麼在幕後,Dart 是如何管理操做序列的執行的呢?

爲了回答這個問題,咱們須要看一下 Dart 的代碼序列器(事件循環)。

當你啓動一個 Flutter(或任何 Dart)應用時,將建立並啓動一個新的線程進程(在 Dart 中爲 「Isolate」)。該線程將是你在整個應用中惟一須要關注的。

因此,此線程建立後,Dart 會自動:

  1. 初始化 2 個 FIFO(先進先出)隊列(「MicroTask」和 「Event」);
  2. 而且當該方法執行完成後,執行 main() 方法,
  3. 啓動事件循環

在該線程的整個生命週期中,一個被稱爲事件循環單一且隱藏的進程將決定你代碼的執行方式及順序(取決於 MicroTaskEvent 隊列)。

事件循環是一種無限循環(由一個內部時鐘控制),在每一個時鐘週期內若是沒有其餘 Dart 代碼執行,則執行如下操做:

void eventLoop(){
    while (microTaskQueue.isNotEmpty){
        fetchFirstMicroTaskFromQueue();
        executeThisMicroTask();
        return;
    }

    if (eventQueue.isNotEmpty){
        fetchFirstEventFromQueue();
        executeThisEventRelatedCode();
    }
}
複製代碼

正如咱們看到的,MicroTask 隊列優先於 Event 隊列,那這 2 個隊列的做用是什麼呢?

MicroTask 隊列

MicroTask 隊列用於很是簡短且須要異步執行的內部動做,這些動做須要在其餘事情完成以後並在將執行權送還給 Event 隊列以前運行。

做爲 MicroTask 的一個例子,你能夠設想必須在資源關閉後當即釋放它。因爲關閉過程可能須要一些時間才能完成,你能夠按照如下方式編寫代碼:

MyResource myResource;

...

void closeAndRelease() {
    scheduleMicroTask(_dispose);
    _close();
}

void _close(){
    // 代碼以同步的方式運行
    // 以關閉資源
    ...
}

void _dispose(){
    // 代碼在
    // _close() 方法
    // 完成後執行
}
複製代碼

這是大多數時候你沒必要使用的東西。好比,在整個 Flutter 源代碼中 scheduleMicroTask() 方法僅被引用了 7 次。

最好優先考慮使用 Event 隊列。

Event 隊列

Event 隊列適用於如下參考模型

  • 外部事件如
    • I/O;
    • 手勢;
    • 繪圖;
    • 計時器;
    • 流;
    • ……
  • futures

事實上,每次外部事件被觸發時,要執行的代碼都會被 Event 隊列所引用。

一旦沒有任何 micro task 運行,事件循環將考慮 Event 隊列中的第一項並執行它。

值得注意的是,Future 操做也經過 Event 隊列處理。


Future

Future 是一個異步執行而且在將來的某一個時刻完成(或失敗)的任務

當你實例化一個 Future 時:

  • Future 的一個實例被建立並記錄在由 Dart 管理的內部數組中;
  • 須要由此 Future 執行的代碼直接推送到 Event 隊列中去;
  • future 實例 返回一個狀態(= incomplete);
  • 若是存在下一個同步代碼,執行它(非 Future 的執行代碼

只要事件循環Event 循環中獲取它,被 Future 引用的代碼將像其餘任何 Event 同樣執行。

當該代碼將被執行並將完成(或失敗)時,then()catchError() 方法將直接被觸發。

爲了說明這一點,咱們來看下面的例子:

void main(){
    print('Before the Future');
    Future((){
        print('Running the Future');
    }).then((_){
        print('Future is complete');
    });
    print('After the Future');
}
複製代碼

若是咱們運行該代碼,輸出將以下所示:

Before the Future
After the Future
Running the Future
Future is complete
複製代碼

這是徹底正確的,由於執行流程以下:

  1. print(‘Before the Future’)
  2. (){print(‘Running the Future’);} 添加到 Event 隊列;
  3. print(‘After the Future’)
  4. 事件循環獲取(在第二步引用的)代碼並執行它
  5. 當代碼執行時,它會查找 then() 語句並執行它

須要記住一些很是重要的事情:

Future 並不是並行執行,而是遵循事件循環處理事件的順序規則執行。


Async 方法

當你使用 async 關鍵字做爲方法聲明的後綴時,Dart 會將其理解爲:

  • 該方法的返回值是一個 Future
  • 同步執行該方法的代碼直到第一個 await 關鍵字,而後它暫停該方法其餘部分的執行;
  • 一旦由 await 關鍵字引用的 Future 執行完成,下一行代碼將當即執行。

瞭解這一點是很是重要的,由於不少開發者認爲 await 暫停了整個流程直到它執行完成,但事實並不是如此。他們忘記了事件循環的運做模式……

爲了更好地進行說明,讓咱們經過如下示例並嘗試指出其運行的結果。

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  Future((){                // <== 該代碼將在將來的某個時間段執行
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

  print('C end from $from');
}

methodD(){
  print('D');
}
複製代碼

正確的順序是:

  1. A
  2. B start
  3. C start from B
  4. C end from B
  5. B end
  6. C start from main
  7. C end from main
  8. D
  9. C running Future from B
  10. C end of Future from B
  11. C running Future from main
  12. C end of Future from main

如今,讓咱們認爲上述代碼中的 methodC() 爲對服務端的調用,這可能須要不均勻的時間來進行響應。我相信能夠很明確地說,預測確切的執行流程可能變得很是困難。

若是你最初但願示例代碼中僅在全部代碼末尾執行 methodD() ,那麼你應該按照如下方式編寫代碼:

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  await Future((){                  // <== 在此處進行修改
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });
  print('C end from $from');
}

methodD(){
  print('D');
}
複製代碼

輸出序列爲:

  1. A
  2. B start
  3. C start from B
  4. C running Future from B
  5. C end of Future from B
  6. C end from B
  7. B end
  8. C start from main
  9. C running Future from main
  10. C end of Future from main
  11. C end from main
  12. D

事實是經過在 methodC() 中定義 Future 的地方簡單地添加 await 會改變整個行爲。

另外,需特別謹記:

async 並不是並行執行,也是遵循事件循環處理事件的順序規則執行。

我想向你演示的最後一個例子以下。 運行 method1method2 的輸出是什麼?它們會是同樣的嗎?

void method1(){
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  myArray.forEach((String value) async {
    await delayedPrint(value);
  });
  print('end of loop');
}

void method2() async {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  for(int i=0; i<myArray.length; i++) {
    await delayedPrint(myArray[i]);
  }
  print('end of loop');
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print('delayedPrint: $value');
}
複製代碼

答案:

method1() method2()
1. before loop 1. before loop
2. end of loop 2. delayedPrint: a (after 1 second)
3. delayedPrint: a (after 1 second) 3. delayedPrint: b (1 second later)
4. delayedPrint: b (directly after) 4. delayedPrint: c (1 second later)
5. delayedPrint: c (directly after) 5. end of loop (right after)

你是否清楚它們行爲不同的區別以及緣由呢?

答案基於這樣一個事實,method1 使用 forEach() 函數來遍歷數組。每次迭代時,它都會調用一個被標記爲 async(所以是一個 Future)的新回調函數。執行該回調直到遇到 await,然後將剩餘的代碼推送到 Event 隊列。一旦迭代完成,它就會執行下一個語句:「print(‘end of loop’)」。執行完成後,事件循環 將處理已註冊的 3 個回調。

對於 method2,全部的內容都運行在一個相同的代碼「塊」中,所以可以一行一行按照順序執行(在本例中)。

正如你所看到的,即便在看起來很是簡單的代碼中,咱們仍然須要牢記事件循環的工做方式……


多線程

所以,咱們在 Flutter 中如何並行運行代碼呢?這可能嗎?

是的,這多虧了 Isolates


Isolate 是什麼?

正如前面解釋過的, IsolateDart 中的 線程

然而,它與常規「線程」的實現存在較大差別,這也是將其命名爲「Isolate」的緣由。

「Isolate」在 Flutter 中並不共享內存。不一樣「Isolate」之間經過「消息」進行通訊。


每一個 Isolate 都有本身的事件循環

每一個「Isolate」都擁有本身的「事件循環」及隊列(MicroTask 和 Event)。這意味着在一個 Isolate 中運行的代碼與另一個 Isolate 不存在任何關聯。

多虧了這一點,咱們能夠得到並行處理的能力。


如何啓動 Isolate?

根據你運行 Isolate 的場景,你可能須要考慮不一樣的方法。

1. 底層解決方案

第一個解決方案不依賴任何軟件包,它徹底依賴 Dart 提供的底層 API。

1.1. 第一步:建立並握手

如前所述,Isolate 不共享任何內存並經過消息進行交互,所以,咱們須要找到一種方法在「調用者」與新的 isolate 之間創建通訊。

每一個 Isolate 都暴露了一個將消息傳遞給 Isolate 的被稱爲「SendPort」的端口。(我的以爲該名字有一些誤導,由於它是一個接收/監聽的端口,但這畢竟是官方名稱)。

這意味着「調用者」和「新的 isolate」須要互相知道彼此的端口才能進行通訊。這個握手的過程以下所示:

//
// 新的 isolate 端口
// 該端口將在將來使用
// 用來給 isolate 發送消息
//
SendPort newIsolateSendPort;

//
// 新 Isolate 實例
//
Isolate newIsolate;

//
// 啓動一個新的 isolate
// 而後開始第一次握手
//
//
void callerCreateIsolate() async {
    //
    // 本地臨時 ReceivePort
    // 用於檢索新的 isolate 的 SendPort
    //
    ReceivePort receivePort = ReceivePort();

    //
    // 初始化新的 isolate
    //
    newIsolate = await Isolate.spawn(
        callbackFunction,
        receivePort.sendPort,
    );

    //
    // 檢索要用於進一步通訊的端口
    //
    //
    newIsolateSendPort = await receivePort.first;
}

//
// 新 isolate 的入口
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 一個 SendPort 實例,用來接收來自調用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向調用者提供此 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 進一步流程
    //
}
複製代碼

約束 isolate 的「入口必須是頂級函數或靜態方法。

1.2. 第二步:向 Isolate 提交消息

如今咱們有了向 Isolate 發送消息的端口,讓咱們看看如何作到這一點:

//
// 向新 isolate 發送消息並接收回復的方法
//
//
// 在該例中,我將使用字符串進行通訊操做
// (發送和接收的數據)
//
Future<String> sendReceive(String messageToBeSent) async {
    //
    // 建立一個臨時端口來接收回復
    //
    ReceivePort port = ReceivePort();

    //
    // 發送消息到 Isolate,而且
    // 通知該 isolate 哪一個端口是用來提供
    // 回覆的
    //
    newIsolateSendPort.send(
        CrossIsolatesMessage<String>(
            sender: port.sendPort,
            message: messageToBeSent,
        )
    );

    //
    // 等待回覆並返回
    //
    return port.first;
}

//
// 擴展回調函數來處理接輸入報文
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 初始化一個 SendPort 來接收來自調用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向調用者提供該 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 監聽輸入報文、處理並提供回覆的
    // Isolate 主程序
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

        //
        // 處理消息
        //
        String newMessage = "complemented string " + incomingMessage.message;

        //
        // 發送處理的結果
        //
        incomingMessage.sender.send(newMessage);
    });
}

//
// 幫助類
//
class CrossIsolatesMessage<T> {
    final SendPort sender;
    final T message;

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
複製代碼
1.3. 第三步:銷燬這個新的 Isolate 實例

當你再也不須要這個新的 Isolate 實例時,最好經過如下方法釋放它:

//
// 釋放一個 isolate 的例程
//
void dispose(){
    newIsolate?.kill(priority: Isolate.immediate);
    newIsolate = null;
}
複製代碼
1.4. 特別說明 - 單監聽器流

你可能已經注意到咱們正在使用在「調用者」和新 isolate 之間進行通訊。這些的類型爲:「單監聽器」流。


2. 一次性計算

若是你只須要運行一些代碼來完成一些特定的工做,而且在工做完成以後不須要與 Isolate 進行交互,那麼這裏有一個很是方便的稱爲 computeHelper

主要包含如下功能:

  • 產生一個 Isolate
  • 在該 isolate 上運行一個回調函數,並傳遞一些數據,
  • 返回回調函數的處理結果,
  • 回調執行後終止 Isolate

約束

「回調」函數必須是頂級函數而且不能是閉包或類中的方法(靜態或非靜態)。


3. 重要限制

在撰寫本文時,發現這點十分重要

Platform-Channel 通訊僅僅主 isolate 支持。該主 isolate 對應於應用啓動時建立的 isolate

也就是說,經過編程建立的 isolate 實例,沒法實現 Platform-Channel 通訊……

不過,仍是有一個解決方法的……請參考此鏈接以得到關於此主題的討論。


我應該何時使用 Futures 和 Isolate?

用戶將根據不一樣的因素來評估應用的質量,好比:

  • 特性
  • 外觀
  • 用戶友好性
  • ……

你的應用能夠知足以上全部因素,但若是用戶在一些處理過程當中遇到了卡頓,這極有可能對你不利。

所以,如下是你在開發過程當中應該系統考慮的一些點:

  1. 若是代碼片斷不能被中斷,使用傳統的同步過程(一個或多個相互調用的方法);
  2. 若是代碼片斷能夠獨立運行而不影響應用的性能,能夠考慮經過 Future 使用事件循環
  3. 若是繁重的處理可能須要一些時間才能完成,而且可能影響應用的性能,考慮使用 Isolate

換句話說,建議儘量地使用 Future(直接或間接地經過 async 方法),由於一旦事件循環擁有空閒時間,這些 Future 的代碼就會被執行。這將使用戶感受事情正在被並行處理(而咱們如今知道事實並不是如此)。

另一個能夠幫助你決定使用 FutureIsolate 的因素是運行某些代碼所須要的平均時間。

  • 若是一個方法須要幾毫秒 => Future
  • 若是一個處理流程須要幾百毫秒 => Isolate

如下是一些很好的 Isolate 選項:

  • JSON 解碼:解碼 JSON(HttpRequest 的響應)可能須要一些時間 => 使用 compute
  • 加密:加密可能很是耗時 => Isolate
  • 圖像處理:處理圖像(好比:剪裁)確實須要一些時間來完成 => Isolate
  • 從 Web 加載圖像:該場景下,爲何不將它委託給一個徹底加載後返回完整圖像的 Isolate

結論

我認爲了解事件循環的工做原理很是重要。

一樣重要的是要謹記 FlutterDart)是單線程的,所以,爲了取悅用戶,開發者必須確保應用運行儘量流暢。FutureIsolate 是很是強大的工具,它們能夠幫助你實現這一目標。

請繼續關注新文章,同時……祝你編程愉快!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索