Please contact: wechat: Michaelzhoujay for reprint

The original article can be accessedMy blog


This article mainly introduces the Event Loop in Flutter and how to parallel processing in Flutter.

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.

Similar to AndroidVM, when you start a Flutter App, the system will start a DartVM. After the DartVM in a Flutter is started, a new Thread will be created and only one Thread will be running on its Isolate.

When the Thread is created, DartVM automatically does three things:

  • Initialize two queues, one called “MicroTask” and one called “Event”, both FIFO queues
  • Execute the main() method and, once done, proceed to the next step
  • Start the Event Loop

The Event Loop is like an infinite Loop, tuned by the internal clock, and each tick, if no other Dart Code is executing, does the following (pseudocode) :

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

    if(eventQueue.isNotEmpty){ fetchFirstEventFromQueue(); executeThisEventRelatedCode(); }}Copy the code

MicroTask Queue

MicroTask Queues are designed for very transient internal operations. After the rest of the Dart code has run and before it is handed over to the Event Queue.

For example, after closing a resource, dispose of some handles. In this example, scheduleMicroTask can be used to dispose of it:

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
    }
Copy the code

Here, although scheduleMicroTask(_Dispose) statement precedes _close(), _close() is executed first and Event loop’s microTask is executed because, as mentioned above, “after the rest of the Dart code has executed.”

Even if you already know when microTasks are executed and have learned to use them with scheduleMicroTask, microTasks are not something you use very often. For the Flutter itself, the entire source code only references scheduleMicroTask() 7 times.

Event Queue

The Event Queue is mainly used to process the operations to be invoked after certain events occur. These events are divided into:

  • External events:
    • I/O
    • gesture
    • drawing
    • timers
    • streams
  • futures

In fact, whenever an external Event occurs, the code to execute is found in the Event Queue. Whenever there are no more MicroTasks to run, the Event Queue will start processing from the first Event

Futures

When you create an instance of a Future, you actually do the following:

  • An instance is created, placed into an internal array and reordered, managed by DART
  • Code that needs to be executed later is pushed directly into the Event Queue
  • The future immediately synchronizes and returns a stateincomplete
  • If there’s anything elsesynchronousCode that executes the Sychronous code first

A Future, like any other Event, will be executed in the EventQueue. The following example illustrates how a Future executes like the Event above

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

After executing, you get the following output:

Before the Future
After the Future
Running the Future
Future is complete
Copy the code

Let’s explain how the code is executed step by step:

  1. Print (‘ Before the Future ‘)
  2. Function “(){print(‘ Running the Future ‘); } “to the event Queue
  3. Print (‘ After the Future ‘)
  4. The Event Loop takes the code described in step 2 and runs it
  5. When the code is finished executing, it tries to find the THEN statement and run it

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

Async Methods

If you add async to the declaration part of any method, you’re actually saying to DART:

  • The result of this method is a Future
  • If an await is encountered during the call, it executes synchronously and pauses the code context in which it resides
  • The next line of code will wait until the future above is finished

Isolate

Each thread has its own Isolate. You can create an Isolate using isolate. spawn or compute. Each Isolate has its own Data, and Event loop is used to communicate with each other through messages

Isolate.spawn(
  aFunctionToRun,
  {'data' : 'Here is some data.'});Copy the code
compute(
  (paramas) {
    /* do something */
  },
  {'data' : 'Here is some data.'});Copy the code

When you create a new Isolate in an Isolate and need to communicate with the new Isolate, you need SendPort and ReceivePort. In order to communicate, the two Isolate must know each other’s ports:

  • Local Isolate PassSendPortTo receive/send messages, the official name is really a bit confusing.
  • When you create an Isolate, you need to givespawnMethod passes aReceivePortThe local Isolate sendPort will be used to send/receive messages and sendPort will be used to return the local Isolate sendPort

Find an example and feel it

//
// 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
    //
}
Copy the code

Both Isolate now have their own ports, so they can start sending messages to each other:

The local Isolate sends a message to the new Isolate and retrieves the result:

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;
}
Copy the code

Local Isolate passively receives messages. Remember the spawn method from above, the first parameter is callbackFunction. This method is used to receive results:

//
// 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,
    });
}
Copy the code

Isolate the destruction

If the Isolate you created is no longer in use, it is best to release it:

voiddispose(){ newIsolate? .kill(priority: Isolate.immediate); newIsolate =null;
}
Copy the code

Single-Listener Streams

In fact, communication between the Isolate is achieved through “single-listener” Streams.

compute

The way to create Isolate is described above, where compute is good for executing tasks after you create them, and you don’t want any communication after the task is done. Compute is a function:

  • Spawn a Isolate
  • Run a callback, pass some data, and return the result
  • When the callback is complete, kill the ISOLATE

Suitable for some scenarios of Isolate

  • JSON parsing
  • Encryption and decryption
  • Image processing, like the cropping
  • Load images from the network

How to choose?

In general:

  • If a method takes tens of milliseconds, use Future
  • If an operation takes hundreds of milliseconds, use Isolate