Flutter 異步編程:Futures、Isolates、Event Loop[轉(zhuǎn)載]

Dart 是一種單線程語言

首先,大家需要牢記,Dart 是單線程的并且 Flutter 依賴于 Dart。

重點(diǎn)
Dart 同一時(shí)刻只執(zhí)行一個(gè)操作,其他操作在該操作之后執(zhí)行,這意味著只要一個(gè)操作正在執(zhí)行,它就不會(huì)被其他 Dart 代碼中斷。
也就是說,如果你考慮純粹的同步方法,那么在它完成之前,后者將是唯一要執(zhí)行的方法。

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

在上面的例子中,myBigLoop() 方法在執(zhí)行完成前永遠(yuǎn)不會(huì)被中斷。因此,如果該方法需要一些時(shí)間,那么在整個(gè)方法執(zhí)行期間應(yīng)用將會(huì)被阻塞。

Dart 執(zhí)行模型

那么在幕后,Dart 是如何管理操作序列的執(zhí)行的呢?

為了回答這個(gè)問題,我們需要看一下 Dart 的代碼序列器(事件循環(huán))。

當(dāng)你啟動(dòng)一個(gè) Flutter(或任何 Dart)應(yīng)用時(shí),將創(chuàng)建并啟動(dòng)一個(gè)新的線程進(jìn)程(在 Dart 中為 「Isolate」)。該線程將是你在整個(gè)應(yīng)用中唯一需要關(guān)注的。

所以,此線程創(chuàng)建后,Dart 會(huì)自動(dòng):

1、初始化 2 個(gè) FIFO(先進(jìn)先出)隊(duì)列(「MicroTask」和 「Event」);
2、并且當(dāng)該方法執(zhí)行完成后,執(zhí)行 main() 方法,
3、啟動(dòng)事件循環(huán)。
在該線程的整個(gè)生命周期中,一個(gè)被稱為事件循環(huán)的單一且隱藏的進(jìn)程將決定你代碼的執(zhí)行方式及順序(取決于 MicroTask 和 Event 隊(duì)列)。

事件循環(huán)是一種無限循環(huán)(由一個(gè)內(nèi)部時(shí)鐘控制),在每個(gè)時(shí)鐘周期內(nèi),如果沒有其他 Dart 代碼執(zhí)行,則執(zhí)行以下操作:

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

    if (eventQueue.isNotEmpty){
        fetchFirstEventFromQueue();
        executeThisEventRelatedCode();
    }
}

正如我們看到的,MicroTask 隊(duì)列優(yōu)先于 Event 隊(duì)列,那這 2 個(gè)隊(duì)列的作用是什么呢?

MicroTask隊(duì)列

MicroTask 隊(duì)列用于非常簡(jiǎn)短且需要異步執(zhí)行的內(nèi)部動(dòng)作,這些動(dòng)作需要在其他事情完成之后并在將執(zhí)行權(quán)送還給 Event 隊(duì)列之前運(yùn)行。

作為 MicroTask 的一個(gè)例子,你可以設(shè)想必須在資源關(guān)閉后立即釋放它。由于關(guān)閉過程可能需要一些時(shí)間才能完成,你可以按照以下方式編寫代碼:

MyResource myResource;

...

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

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

void _dispose(){
    // 代碼在
    // _close() 方法
    // 完成后執(zhí)行
}

這是大多數(shù)時(shí)候你不必使用的東西。比如,在整個(gè) Flutter 源代碼中 scheduleMicroTask() 方法僅被引用了 7 次。

最好優(yōu)先考慮使用 Event 隊(duì)列。

Event 隊(duì)列

Event 隊(duì)列適用于以下參考模型

  • 外部事件如

    • I/O;
    • 手勢(shì);
    • 繪圖;
    • 計(jì)時(shí)器;
    • 流;
      ……
  • futures

事實(shí)上,每次外部事件被觸發(fā)時(shí),要執(zhí)行的代碼都會(huì)被 Event 隊(duì)列所引用。

一旦沒有任何 micro task 運(yùn)行,事件循環(huán)將考慮 Event 隊(duì)列中的第一項(xiàng)并執(zhí)行它。

值得注意的是,F(xiàn)uture 操作也通過 Event 隊(duì)列處理。

Future

Future 是一個(gè)異步執(zhí)行并且在未來的某一個(gè)時(shí)刻完成(或失?。┑娜蝿?wù)。

當(dāng)你實(shí)例化一個(gè) Future 時(shí):

  • 該 future 的一個(gè)實(shí)例被創(chuàng)建并記錄在由 Dart 管理的內(nèi)部數(shù)組中;
  • 需要由此 Future 執(zhí)行的代碼直接推送到 Event 隊(duì)列中去;
  • 該 future 實(shí)例 返回一個(gè)狀態(tài)(= incomplete);
  • 如果存在下一個(gè)同步代碼,執(zhí)行它(非 future 的執(zhí)行代碼)
  • 只要事件循環(huán)從 Event 循環(huán)中獲取它,被 future 引用的代碼將像其他任何 Event 一樣執(zhí)行。

當(dāng)該代碼將被執(zhí)行并將完成(或失敗)時(shí),then() 或 catchError() 方法將直接被觸發(fā)。

為了說明這一點(diǎn),我們來看下面的例子:

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

如果我們運(yùn)行該代碼,輸出將如下所示:

Before the Future
After the Future
Running the Future
Future is complete

執(zhí)行流程如下:
1、print(‘Before the Future’)
2、將 (){print(‘Running the Future’);} 添加到 Event 隊(duì)列;
3、print(‘After the Future’)
4、事件循環(huán)獲取(在第二步引用的)代碼并執(zhí)行它
5、當(dāng)代碼執(zhí)行時(shí),它會(huì)查找 then() 語句并執(zhí)行它
需要記住一些非常重要的事情:

Future 并非并行執(zhí)行,而是遵循事件循環(huán)處理事件的順序規(guī)則執(zhí)行。

Async 方法

當(dāng)你使用 async 關(guān)鍵字作為方法聲明的后綴時(shí),Dart 會(huì)將其理解為:

  • 該方法的返回值是一個(gè) Future;
  • 它同步執(zhí)行該方法的代碼直到第一個(gè) await 關(guān)鍵字,然后它暫停該方法其他部分的執(zhí)行;
  • 一旦由 await 關(guān)鍵字引用的 Future 執(zhí)行完成,下一行代碼將立即執(zhí)行。
    了解這一點(diǎn)是非常重要的,因?yàn)楹芏嚅_發(fā)者認(rèn)為 await 暫停了整個(gè)流程直到它執(zhí)行完成,但事實(shí)并非如此。他們忘記了事件循環(huán)的運(yùn)作模式……

為了更好地進(jìn)行說明,讓我們通過以下示例并嘗試指出其運(yùn)行的結(jié)果。

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((){                // <== 該代碼將在未來的某個(gè)時(shí)間段執(zhí)行
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

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

methodD(){
  print('D');
}

正確的順序是:

A
B start
C start from B
C end from B
B end
C start from main
C end from main
D
C running Future from B
C end of Future from B
C running Future from main
C end of Future from main

現(xiàn)在,讓我們認(rèn)為上述代碼中的 methodC() 為對(duì)服務(wù)端的調(diào)用,這可能需要不均勻的時(shí)間來進(jìn)行響應(yīng)。我相信可以很明確地說,預(yù)測(cè)確切的執(zhí)行流程可能變得非常困難。

如果你最初希望示例代碼中僅在所有代碼末尾執(zhí)行 methodD() ,那么你應(yīng)該按照以下方式編寫代碼:

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((){                  // <== 在此處進(jìn)行修改
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });
  print('C end from $from');
}

methodD(){
  print('D');
}

輸出序列為:

A
B start
C start from B
C running Future from B
C end of Future from B
C end from B
B end
C start from main
C running Future from main
C end of Future from main
C
D

事實(shí)是通過在 methodC() 中定義 Future 的地方簡(jiǎn)單地添加 await 會(huì)改變整個(gè)行為。

另外,需特別謹(jǐn)記:

async 并非并行執(zhí)行,也是遵循事件循環(huán)處理事件的順序規(guī)則執(zhí)行。

我想向你演示的最后一個(gè)例子如下。 運(yùn)行 method1 和 method2 的輸出是什么?它們會(huì)是一樣的嗎?

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) |

你是否清楚它們行為不一樣的區(qū)別以及原因呢?

答案基于這樣一個(gè)事實(shí),method1 使用 forEach() 函數(shù)來遍歷數(shù)組。每次迭代時(shí),它都會(huì)調(diào)用一個(gè)被標(biāo)記為 async(因此是一個(gè) Future)的新回調(diào)函數(shù)。執(zhí)行該回調(diào)直到遇到 await,而后將剩余的代碼推送到 Event 隊(duì)列。一旦迭代完成,它就會(huì)執(zhí)行下一個(gè)語句:“print(‘end of loop’)”。執(zhí)行完成后,事件循環(huán) 將處理已注冊(cè)的 3 個(gè)回調(diào)。

對(duì)于 method2,所有的內(nèi)容都運(yùn)行在一個(gè)相同的代碼「塊」中,因此能夠一行一行按照順序執(zhí)行(在本例中)。

正如你所看到的,即使在看起來非常簡(jiǎn)單的代碼中,我們?nèi)匀恍枰斡浭录h(huán)的工作方式……

多線程

因此,我們?cè)?Flutter 中如何并行運(yùn)行代碼呢?這可能嗎?是的,這多虧了 Isolates。

Isolate 是什么?

正如前面解釋過的, Isolate 是 Dart 中的 線程。

然而,它與常規(guī)「線程」的實(shí)現(xiàn)存在較大差異,這也是將其命名為「Isolate」的原因。

「Isolate」在 Flutter 中并不共享內(nèi)存。不同「Isolate」之間通過「消息」進(jìn)行通信。

每個(gè) Isolate 都有自己的事件循環(huán)

每個(gè)「Isolate」都擁有自己的「事件循環(huán)」及隊(duì)列(MicroTask 和 Event)。這意味著在一個(gè) Isolate 中運(yùn)行的代碼與另外一個(gè) Isolate 不存在任何關(guān)聯(lián)。

多虧了這一點(diǎn),我們可以獲得并行處理的能力。

如何啟動(dòng) Isolate?

根據(jù)你運(yùn)行 Isolate 的場(chǎng)景,你可能需要考慮不同的方法。

1. 底層解決方案

第一個(gè)解決方案不依賴任何軟件包,它完全依賴 Dart 提供的底層 API。

1.1. 第一步:創(chuàng)建并握手

如前所述,Isolate 不共享任何內(nèi)存并通過消息進(jìn)行交互,因此,我們需要找到一種方法在「調(diào)用者」與新的 isolate 之間建立通信。

每個(gè) Isolate 都暴露了一個(gè)將消息傳遞給 Isolate 的被稱為「SendPort」的端口。(個(gè)人覺得該名字有一些誤導(dǎo),因?yàn)樗且粋€(gè)接收/監(jiān)聽的端口,但這畢竟是官方名稱)。

這意味著「調(diào)用者」和「新的 isolate」需要互相知道彼此的端口才能進(jìn)行通信。這個(gè)握手的過程如下所示:

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

//
// 新 Isolate 實(shí)例
//
Isolate newIsolate;

//
// 啟動(dòng)一個(gè)新的 isolate
// 然后開始第一次握手
//
//
void callerCreateIsolate() async {
    //
    // 本地臨時(shí) ReceivePort
    // 用于檢索新的 isolate 的 SendPort
    //
    ReceivePort receivePort = ReceivePort();

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

    //
    // 檢索要用于進(jìn)一步通信的端口
    //
    //
    newIsolateSendPort = await receivePort.first;
}

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

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

    //
    // 進(jìn)一步流程
    //
}

約束 isolate 的「入口」必須是頂級(jí)函數(shù)或靜態(tài)方法。

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

現(xiàn)在我們有了向 Isolate 發(fā)送消息的端口,讓我們看看如何做到這一點(diǎn):

//
// 向新 isolate 發(fā)送消息并接收回復(fù)的方法
//
//
// 在該例中,我將使用字符串進(jìn)行通信操作
// (發(fā)送和接收的數(shù)據(jù))
//
Future<String> sendReceive(String messageToBeSent) async {
    //
    // 創(chuàng)建一個(gè)臨時(shí)端口來接收回復(fù)
    //
    ReceivePort port = ReceivePort();

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

    //
    // 等待回復(fù)并返回
    //
    return port.first;
}

//
// 擴(kuò)展回調(diào)函數(shù)來處理接輸入報(bào)文
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 初始化一個(gè) SendPort 來接收來自調(diào)用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

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

    //
    // 監(jiān)聽輸入報(bào)文、處理并提供回復(fù)的
    // Isolate 主程序
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

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

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

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

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
1.3. 第三步:銷毀這個(gè)新的 Isolate 實(shí)例

當(dāng)你不再需要這個(gè)新的 Isolate 實(shí)例時(shí),最好通過以下方法釋放它:

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

你可能已經(jīng)注意到我們正在使用流在「調(diào)用者」和新 isolate 之間進(jìn)行通信。這些流的類型為:「單監(jiān)聽器」流。

2. 一次性計(jì)算

如果你只需要運(yùn)行一些代碼來完成一些特定的工作,并且在工作完成之后不需要與 Isolate 進(jìn)行交互,那么這里有一個(gè)非常方便的稱為 computeHelper。

主要包含以下功能:

  • 產(chǎn)生一個(gè) Isolate
  • 在該 isolate 上運(yùn)行一個(gè)回調(diào)函數(shù),并傳遞一些數(shù)據(jù),
  • 返回回調(diào)函數(shù)的處理結(jié)果,
  • 回調(diào)執(zhí)行后終止 Isolate。

約束
「回調(diào)」函數(shù)必須是頂級(jí)函數(shù)并且不能是閉包或類中的方法(靜態(tài)或非靜態(tài))。

3. 重要限制

在撰寫本文時(shí),發(fā)現(xiàn)這點(diǎn)十分重要

Platform-Channel 通信僅僅主 isolate 支持。該主 isolate 對(duì)應(yīng)于應(yīng)用啟動(dòng)時(shí)創(chuàng)建的 isolate。

也就是說,通過編程創(chuàng)建的 isolate 實(shí)例,無法實(shí)現(xiàn) Platform-Channel 通信……

不過,還是有一個(gè)解決方法的……請(qǐng)參考此連接以獲得關(guān)于此主題的討論。

我應(yīng)該什么時(shí)候使用 Futures 和 Isolate?

用戶將根據(jù)不同的因素來評(píng)估應(yīng)用的質(zhì)量,比如:

  • 特性
  • 外觀
  • 用戶友好性
    ……
    你的應(yīng)用可以滿足以上所有因素,但如果用戶在一些處理過程中遇到了卡頓,這極有可能對(duì)你不利。

因此,以下是你在開發(fā)過程中應(yīng)該系統(tǒng)考慮的一些點(diǎn):
1、如果代碼片段不能被中斷,使用傳統(tǒng)的同步過程(一個(gè)或多個(gè)相互調(diào)用的方法);
2、如果代碼片段可以獨(dú)立運(yùn)行而不影響應(yīng)用的性能,可以考慮通過 Future 使用事件循環(huán);
3、如果繁重的處理可能需要一些時(shí)間才能完成,并且可能影響應(yīng)用的性能,考慮使用 Isolate。

換句話說,建議盡可能地使用 Future(直接或間接地通過 async 方法),因?yàn)橐坏┦录h(huán)擁有空閑時(shí)間,這些 Future 的代碼就會(huì)被執(zhí)行。這將使用戶感覺事情正在被并行處理(而我們現(xiàn)在知道事實(shí)并非如此)。

另外一個(gè)可以幫助你決定使用 Future 或 Isolate 的因素是運(yùn)行某些代碼所需要的平均時(shí)間。

  • 如果一個(gè)方法需要幾毫秒 => Future
  • 如果一個(gè)處理流程需要幾百毫秒 => Isolate

以下是一些很好的 Isolate 選項(xiàng):

  • JSON 解碼:解碼 JSON(HttpRequest 的響應(yīng))可能需要一些時(shí)間 => 使用 compute
  • 加密:加密可能非常耗時(shí) => Isolate
  • 圖像處理:處理圖像(比如:剪裁)確實(shí)需要一些時(shí)間來完成 => Isolate
  • 從 Web 加載圖像:該場(chǎng)景下,為什么不將它委托給一個(gè)完全加載后返回完整圖像的 Isolate?
結(jié)論

我認(rèn)為了解事件循環(huán)的工作原理非常重要。

同樣重要的是要謹(jǐn)記 Flutter(Dart)是單線程的,因此,為了取悅用戶,開發(fā)者必須確保應(yīng)用運(yùn)行盡可能流暢。Future 和 Isolate 是非常強(qiáng)大的工具,它們可以幫助你實(shí)現(xiàn)這一目標(biāo)。

原文地址:Futures - Isolates - Event Loop
原文作者:www.didierboelens.com
譯文出自:掘金翻譯計(jì)劃
本文永久鏈接:https://github.com/xitu/gold-miner/blob/master/TODO1/futures-isolates-event-loop.md
譯者:nanjingboy
校對(duì)者:sunui, Fengziyin1234

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。