• B: foreign-exchange visits are prolonged and debilitating
  • Original author: www.didierboelens.com
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: nanjingboy
  • Proofread by: sunui, Fengziyin1234

This article introduces the different code execution modes of Flutter: single-threaded, multi-threaded, synchronous, and asynchronous.

Difficulty: Intermediate

The profile

I have recently received some questions related to the concepts of Future, Async, await, Isolate and parallel execution.

Because of these problems, some people have trouble handling the order in which code is executed.

I thought it would be useful to have an article that explains these concepts of asynchronous, parallel processing and disambiguates any of them.


Dart is a single-threaded language

First, remember that Dart is single-threaded and that Flutter depends on Dart.

Focus on

Dart only performs one action at a time, and other actions are executed after that action, meaning that as long as an action is being executed, it will not be interrupted by other Dart code.

That is, if you consider a purely synchronous method, the latter will be the only method to execute until it completes.

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

In the example above, the myBigLoop() method is never interrupted until execution is complete. Therefore, if the method takes some time, the application will be blocked during the entire method execution.


DartExecution model

So behind the scenes, how does Dart manage the execution of a sequence of operations?

To answer this question, we need to look at Dart’s code sequencer (event loop).

When you start a Flutter (or any Dart application), a new thread process (” Isolate “in Dart) is created and started. This thread will be your only concern in the entire application.

So, after this thread is created, Dart automatically:

  1. Initialize two FIFO (first in, first out) queues (” MicroTask “and” Event “);
  2. And when the method is done, execute main(),
  3. Start the event loop.

Throughout the life of the thread, a single, hidden process called an Event loop determines how and in what order your code executes (depending on MicroTask and Event queues).

An event loop is an infinite loop (controlled by an internal clock), and within each clock cycle, if no other Dart code executes, the following actions are performed:

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

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

As we have seen, the MicroTask queue takes precedence over the Event queue, so what are the two queues for?

MicroTask queue

MicroTask queues are used for very short internal actions that need to be executed asynchronously, after something else has been done and before execution is returned to the Event queue.

As an example of MicroTask, you can imagine that a resource must be released as soon as it is shut down. Since the shutdown process may take some time to complete, you can write the code as follows:

MyResource myResource; .void closeAndRelease() {
    scheduleMicroTask(_dispose);
    _close();
}

void _close(){
    // The code runs synchronously
    // To close the resource. }void _dispose(){
    / / in the code
    / / _close () method
    // Execute after completion
}
Copy the code

This is something you don’t have to use most of the time. For example, the scheduleMicroTask() method is referenced only 7 times in the entire Flutter source code.

It is best to use the Event queue first.

The Event queue

The Event queue applies to the following reference model

  • External events such as
    • The I/O;
    • Gestures;
    • The drawing;
    • The timer.
    • Flow;
  • futures

In fact, every time an external Event is triggered, the code to execute is referenced by the Event queue.

Once there are no Micro Tasks running, the Event loop will consider the first item in the Event queue and execute it.

It is worth noting that Future operations are also handled through the Event queue.


Future

A Future is a task that executes asynchronously and completes (or fails) at some point in the Future.

When you instantiate a Future:

  • An instance of the Future is created and recorded in an internal array managed by Dart;
  • Code that needs to be executed by this Future is pushed directly to the Event queue;
  • The Future instance returns a state (= incomplete);
  • If the next synchronous code exists, execute it (non-future executable code)

As long as the Event loop fetches it from the Event loop, the code referenced by the Future will execute like any other Event.

When the code will be executed and will complete (or fail), the THEN () or catchError() methods will be fired directly.

To illustrate this point, let’s look at the following example:

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

If we ran the code, the output would look like this:

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

This is entirely true because the execution flow is as follows:

  1. Print (” Before “the” Future “)
  2. Will () {print (‘ Running the Future “); } add to the Event queue;
  3. Print (‘ After the Future ‘
  4. The event loop takes the code referenced in step 2 and executes it
  5. When the code executes, it looks for the THEN () statement and executes it

Some very important things to keep in mind:

Futures are not executed in parallel, but follow the sequential rules that event loops handle events.


Async method

When you use the async keyword as the suffix for a method declaration, Dart will read it as:

  • The return value of this method is a Future;
  • It synchronizes the code executing the method up to the first await keyword, and then it suspends the rest of the method;
  • Once the Future referenced by the await keyword completes, the next line of code executes immediately.

This is important to understand because many developers think that await suspends the whole process until it completes, but this is not the case. They forget how the cycle of events works…

To better illustrate, let’s go through the following example and try to point out the results of its execution.

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((){                // <== This code will be executed at some future time
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

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

methodD(){
  print('D');
}
Copy the code

The correct order is:

  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

For now, let’s think of methodC() in the code above as a call to the server, which may take an uneven time to respond. I believe it is safe to say that predicting the exact flow of execution can become very difficult.

If you initially want methodD() to be executed only at the end of all code in the sample code, you should write the code as follows:

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((){                  // <== change it here
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });
  print('C end from $from');
}

methodD(){
  print('D');
}
Copy the code

The output sequence is:

  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

The fact is that simply adding await where Future is defined in methodC() changes the entire behavior.

Also, keep in mind:

Async is not executed in parallel, but in accordance with the order in which events are processed in an event loop.

The last example I want to show you is as follows. What is the output of running method1 and method2? Could they be the same?

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

The answer:

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)

Do you know the difference between their behavior and why?

The answer is based on the fact that method1 uses the forEach() function to iterate over a set of numbers. With each iteration, it calls a new callback function marked async (and therefore a Future). Execute the callback until you encounter await, and then push the rest of the code to the Event queue. Once the iteration is complete, it executes the next statement: “print(‘ end of loop ‘)”. When the execution is complete, the event loop handles the three registered callbacks.

With Method2, everything runs in the same “block” of code, so it can be executed sequentially line by line (in this case).

As you can see, even in seemingly simple code, we still need to keep in mind how event loops work…


multithreading

So how do we run code in parallel in Flutter? Is that possible?

B: Yes, thanks to Computer campaigns.


What is Isolate?

As explained earlier, Isolate is the thread in Dart.

However, it is quite different from the normal “threading” implementation, which is why it was named “Isolate”.

The “Isolate” does not share memory in Flutter. The Isolate communicates with each other through messages.


Each Isolate has its ownEvent loop

Each Isolate has its own “Event loops” and queues (microtasks and events). This means that code running in one Isolate has no relationship to another.

Thanks to this, we can gain the ability to do parallel processing.


How do I start the Isolate?

Depending on the situation in which you are running Isolate, you may need to consider different approaches.

1. Underlying solutions

The first solution does not rely on any software package; it relies entirely on the underlying API provided by Dart.

1.1. Step 1: Create and shake hands

As mentioned earlier, the Isolate does not share any memory and interacts with messages, so we needed to find a way to establish communication between the “caller” and the new Isolate.

Each Isolate exposes a port called “SendPort” for passing messages to the Isolate. (I think the name is a bit misleading because it’s a receiving/listening port, but that’s the official name).

This means that the “caller” and the “new ISOLATE” need to know each other’s ports in order to communicate. The process of this handshake is as follows:

//
// New ISOLATE port
// This port will be used in the future
// Used to send messages to isolate
//
SendPort newIsolateSendPort;

//
// New Isolate instance
//
Isolate newIsolate;

//
// Start a new ISOLATE
// Then begin the first handshake
//
//
void callerCreateIsolate() async {
    //
    // Local temporary ReceivePort
    SendPort to retrieve the new ISOLATE
    //
    ReceivePort receivePort = ReceivePort();

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

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

//
// Entry to the new ISOLATE
//
static void callbackFunction(SendPort callerSendPort){
    //
    // An instance of SendPort to receive messages from the caller
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // Provide the caller with a SendPort reference to the ISOLATE
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // Further process
    //
}
Copy the code

The “entry” to constrain the ISOLATE must be either top-level functions or static methods.

1.2. Step 2: Submit messages to Isolate

Now that we have a port to send messages to the Isolate, let’s see how to do this:

//
// A method for sending messages to the new ISOLATE and receiving replies
//
//
// In this case, I will use strings for communication
// (data sent and received)
//
Future<String> sendReceive(String messageToBeSent) async {
    //
    // Create a temporary port to receive replies
    //
    ReceivePort port = ReceivePort();

    //
    // Send a message to Isolate and
    // Notify the ISOLATE which port is used to provide
    / / reply
    //
    newIsolateSendPort.send(
        CrossIsolatesMessage<String>(
            sender: port.sendPort,
            message: messageToBeSent,
        )
    );

    //
    // Wait for a reply and return
    //
    return port.first;
}

//
// Extend the callback function to handle incoming messages
//
static void callbackFunction(SendPort callerSendPort){
    //
    // Initialize a SendPort to receive the message from the caller
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // Provide the caller with a SendPort reference to the ISOLATE
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // Listens for incoming messages, processes them, and provides replies
    // Isolate main program
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

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

        //
        // Send the result of the processing
        //
        incomingMessage.sender.send(newMessage);
    });
}

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

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
Copy the code
1.3. Step 3: Destroy the new Isolate instance

When you no longer need a new Isolate instance, it is best to release it by:

//
// Release a isolate routine
//
voiddispose(){ newIsolate? .kill(priority: Isolate.immediate); newIsolate =null;
}
Copy the code
1.4. Special note – Single listener stream

You may have noticed that we are using streams to communicate between the “caller” and the new ISOLATE. These flows are of the type: “single listener” flows.


2. One-time calculation

If you just need to run some code to do some specific work, and you don’t need to interact with the Isolate once the work is done, there’s a very handy Helper called Compute.

It mainly contains the following functions:

  • Make an Isolate,
  • Running a callback function on the ISOLATE and passing some data,
  • Returns the result of processing the callback function,
  • After the callback is executed, the Isolate is terminated.

The constraint

A “callback” function must be a top-level function and cannot be a closure or method in a class (static or non-static).


3. Important limiting

It is important to note this while writing this article

Platform-channel communication is supported only by the main ISOLATE. The main ISOLATE corresponds to the ISOLATE created when the application is started.

That is to say, the isolate instances created programmatically cannot implement platform-channel communication…

There is, however, a solution… Please refer to this link for a discussion on this topic.


When should I use Futures and Isolate?

Users will evaluate the quality of the application based on various factors, such as:

  • features
  • appearance
  • User friendliness

Your app can do all of these things, but if users are stuck in some process, it’s likely to work against you.

Therefore, here are some points you should consider systematically during development:

  1. If the code snippet cannot be broken, use a traditional synchronization procedure (one or more methods that call each other);
  2. If code snippets can run independently without affecting application performance, consider using event loops through futures;
  3. Consider using Isolate if heavy processing may take some time to complete and may affect application performance.

In other words, it is recommended to use Futureswhenever possible (directly or indirectly through async methods), because the code for these futureswill be executed as soon as the event loop has idle time. This gives the user the impression that things are being processed in parallel (which we now know they are not).

Another factor that can help you decide to use Future or Isolate is the average time it takes to run some code.

  • If a method takes milliseconds => Future
  • If a process takes hundreds of milliseconds, go to Isolate

Here are some good Isolate options:

  • JSON decoding: Decoding JSON (HttpRequest response) may take some time => use compute
  • Encryption: Encryption may take a long time. => Isolate
  • Image processing: Processing images (e.g. clipping) does take some time to complete => Isolate
  • Load images from the Web: in this scenario, why not delegate it to a completely loaded Isolate that returns the full image?

conclusion

I think it’s important to understand how the event loop works.

It is also important to remember that Flutter (Dart) is single-threaded, so to please users, developers must ensure that the application runs as smoothly as possible. Future and Isolate are very powerful tools to help you achieve this goal.

Stay tuned for new posts and in the meantime… Have fun programming!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.