Flutter的Event Loop、Future及Isolate

Michael周發表於2019-06-26

轉載請聯絡: 微訊號: michaelzhoujay

原文請訪問我的部落格


本文主要介紹Flutter中Event Loop以及如何在Flutter中做parallel processing.

Event Loop

First things first, everyone needs to bear in mind that Dart is Single Thread and Flutter relies on Dart.

IMPORTANT Dart executes one operation at a time, one after the other meaning that as long as one operation is executing, it cannot be interrupted by any other Dart code.

跟AndroidVM類似,當你啟動一個Flutter的App,那麼就會系統就會啟動一個DartVM Flutter中的Dart VM啟動後,那麼一個新的Thread就會被建立,並且只會有一個執行緒,它執行在自己的Isolate中。

當這個Thread被建立後,DartVM會自動做以下3件事情:

  • 初始化2個佇列,一個叫“MicroTask”,一個叫“Event”,都是FIFO佇列
  • 執行 main() 方法,一旦執行完畢就做下一步
  • 啟動 Event Loop

Event Loop就像一個 infinite loop,被內部時鐘來調諧,每一個tick,如果沒有其他Dart Code在執行,就會做如下的事情(虛擬碼):

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

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

MicroTask Queue

MicroTask Queue是為了非常短暫的asynchronously的內部操作來設計的。在其他Dart程式碼執行完畢後,且在移交給Event Queue前。

舉個例子,我們經常需要在close一個resource以後,dispose掉一些handle,下面的這個例子裡,scheduleMicroTask 可以用來做 dispose 的事情:

    MyResource myResource;

    ...

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

    void _close(){
        // The code to be run synchronously
        // to close the resource
        ...
    }

    void _dispose(){
        // The code which has to be run
        // right after the _close()
        // has completed
    }
複製程式碼

這裡,雖然scheduleMicroTask(_dispose)語句在_close()語句之前,但是由於上面說到的,“其他Dart程式碼執行完畢後”,所以_close()會先執行,然後執行 Event loop 的 microTask。

即使你已經知道 microTask 的執行時機,而且還學習了用scheduleMicroTask來使用 microTask,但是 microTask 也不是你常用的東西。就 Flutter 本身來說,整個 source code只引用了 scheduleMicroTask() 7次。

Event Queue

Event Queue 主要用來處理當某些事件發生後,呼叫哪些操作,這些事件分為:

  • 外部事件:
    • I/O
    • gesture
    • drawing
    • timers
    • streams
  • futures

事實上,每當外部事件發生時,要執行的程式碼都是在 Event Queue裡找到的。 只要當沒有 MicroTask 需要run了,那麼 Event Queue 就會從第一個事件開始處理

Futures

當你建立了一個future的例項,實際上是做了以下幾件事情:

  • 一個 instance 被建立,放到一個內部的陣列後重新排序,由dart管理
  • 需要後續被執行的程式碼,被直接push到 Event Queue裡面
  • future立即同步返回一個狀態incomplete
  • 如果有其他 synchronous 程式碼,會先執行這些 sychronous 程式碼

Future和其他Event一樣,會在EventQueue裡被執行。 以下的例子用來說明Future和上面的Event執行過程一樣

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. 將function “(){print(‘Running the Future’);}” 新增到 event queue
  3. 執行print(‘After the Future’)
  4. Event Loop 取到第2步裡說的程式碼,並且執行
  5. 程式碼執行完畢後,它嘗試找到 then 語句並執行

A Future is NOT executed in parallel but following the regular sequence of events, handled by the Event Loop

Async Methods

如果在任何一個方法的宣告部分加上 async 字尾,那麼你實際上在向dart表明:

  • 該方法的結果是一個 future
  • 如果呼叫時遇到一個await,那麼它會同步執行,會把它所在的程式碼上下文給pause住
  • 下一行程式碼會等待,直到上面的future(被await等的)結束

Isolate

每個執行緒有自己的Isolate,你可以用Isolate.spawn 或者是 compute 來建立一個 Isolate 每個Isolate都有自己的Data,和Event loop Isolate之間通過訊息來進行溝通

Isolate.spawn(
  aFunctionToRun,
  {'data' : 'Here is some data.'}, 
);
複製程式碼
compute(
  (paramas) {
    /* do something */
  },
  {'data' : 'Here is some data.'},
);  
複製程式碼

當你在一個Isolate中,建立了一個新的Isolate,然後要和新的Isolate進行溝通,那麼就需要SendPortReceivePort。 為了能溝通,兩個Isolate必須要互相知曉對方的port:

  • 本地 Isolate 通過 SendPort 來收/發訊息,官方起的名字真的是有些讓人困惑。
  • 當你建立一個Isolate時,就需要給 spawn 方法傳遞一個 ReceivePort 的例項,後續會用這個port來收/發訊息,同時也會通過這個port把本地 Isolate的sendport返回

找了一個例子,感受一下

//
// The port of the new isolate
// this port will be used to further
// send messages to that isolate
//
SendPort newIsolateSendPort;

//
// Instance of the new Isolate
//
Isolate newIsolate;

//
// Method that launches a new isolate
// and proceeds with the initial
// hand-shaking
//
void callerCreateIsolate() async {
    //
    // Local and temporary ReceivePort to retrieve
    // the new isolate's SendPort
    //
    ReceivePort receivePort = ReceivePort();

    //
    // Instantiate the new isolate
    //
    newIsolate = await Isolate.spawn(
        callbackFunction,
        receivePort.sendPort,
    );

    //
    // Retrieve the port to be used for further
    // communication
    //
    newIsolateSendPort = await receivePort.first;
}

//
// The entry point of the new isolate
//
static void callbackFunction(SendPort callerSendPort){
    //
    // Instantiate a SendPort to receive message
    // from the caller
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // Provide the caller with the reference of THIS isolate's SendPort
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // Further processing
    //
}
複製程式碼

兩個 Isolate 都有了各自的port,那麼它們就可以開始互發訊息了:

本地Isolate向新Isolate發訊息並回收結果:

Future<String> sendReceive(String messageToBeSent) async {
    //
    // We create a temporary port to receive the answer
    //
    ReceivePort port = ReceivePort();

    //
    // We send the message to the Isolate, and also
    // tell the isolate which port to use to provide
    // any answer
    //
    newIsolateSendPort.send(
        CrossIsolatesMessage<String>(
            sender: port.sendPort,
            message: messageToBeSent,
        )
    );

    //
    // Wait for the answer and return it
    //
    return port.first;
}
複製程式碼

本地Isolate被動收訊息,還記得上面的spawn方法嗎,第一個引數是 callbackFunction這個方法就是用來收結果的:

//
// Extension of the callback function to process incoming messages
//
static void callbackFunction(SendPort callerSendPort){
    //
    // Instantiate a SendPort to receive message
    // from the caller
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // Provide the caller with the reference of THIS isolate's SendPort
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // Isolate main routine that listens to incoming messages,
    // processes it and provides an answer
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

        //
        // Process the message
        //
        String newMessage = "complemented string " + incomingMessage.message;

        //
        // Sends the outcome of the processing
        //
        incomingMessage.sender.send(newMessage);
    });
}

//
// Helper class
//
class CrossIsolatesMessage<T> {
    final SendPort sender;
    final T message;

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
複製程式碼

Isolate 的銷燬

如果建立的Isolate不再使用,那麼最好是能將其release掉:

void dispose(){
    newIsolate?.kill(priority: Isolate.immediate);
    newIsolate = null;
}
複製程式碼

Single-Listener Streams

實際上Isolate之間的溝通是通過 “Single-Listener” Streams 來實現的。

compute

上面說過建立 Isolate的方式,其中 compute 適合於建立以後執行任務,而且完成任務後你不希望有任何溝通。 compute是個function:

  • spawn 一個 Isolate
  • 執行一個callback,傳遞一些data,返回結果
  • 在callback執行完畢時,kill掉isolate

適合 Isolate 的一些場景

  • JSON 解析
  • encryption 加解密
  • 影象處理,比如 cropping
  • 從網路載入圖片

如何挑選呢?

一般來說:

  • 如果一個方法耗時幾十毫秒,用 Future
  • 如果一個操作需要幾百毫秒了,那麼就用 Isolate

相關文章