The asynchronous programming model in Dart — Event Loop

First, it should be noted that Dart is a single-threaded language

Event Loop

Dart is a single-threaded programming language. If an operation blocks the Dart thread, the application does not progress until the operation is complete. Therefore, for scalability, it is critical that no I/O operations block. Dart: IO does not block I/O operations, but uses an asynchronous programming model inspired by Node.js, EventMachine, and Twisted.

The above is quoted here

The usual way to implement asynchrony is multithreading, and the other is the Asynchronous programming model (Event Loop).

Event Loop

Dart, like most single-threaded languages, uses Event loops for asynchronous programming, which are good for waiting, time-consuming tasks, as opposed to concurrent programming for computationally intensive tasks

Two queues in the Event Loop

An Event loop contains two Event queues: Event Queue and Microtask Queue.

  • Event Queue
    • External events:
      • I/O
      • gestures
      • drawing
      • The timer
      • Steam
      • .
    • Internal events:
      • Future
  • Microtask Queue
    • For very short internal actions that need to be executed asynchronously (such as asynchronous notifications for a Stream)

In most cases, however, we won’t use the Microtask Queue, which is handed over to Dart itself. Microtask Queue has a higher priority than Event Queue

Event Loop execution process

How do I add tasks toMicrotask QueueandEvent Queue

  • Can run directly (not added to any queue)
    • Future.sync()
    • Future.value()
    • _.then()
  • Microtask Queue
    • scheduleMicrotask()
    • Future.microtask()
    • _complete.then()
  • Event Queue
    • Future()
    • Future.delayed()
    • Timer()
Lay special emphasis on:

_.then() : indicates that the Future is not completed. Then, in this case, is not added to any queue, but executed immediately after the Future is completed

_complete.THEN () : to be used in a Future that has already been completed. Then will be added to the Microtask Queue (since the Future is already completed, its THEN will need to be executed as soon as possible, so it will be added to the Queue with a higher priority).

Future.delayed() : added to Event Queue after delayed Duration

Future.delayed(Duration(seconds: 0),()) will also be added to the Event Queue

Timer() : The internal implementation of the Future is the Timer

The explanation for.then() is provided in the source code comments

If this future is already completed, the callback will not be called, immediately, but will be scheduled in a later microtask.

test_complete.then()joinMicrotask QueueIn the case
void main() { scheduleMicrotask(() => print('Microtask 1')); Future.microtask(() => print('Microtask 2')); // Since future.value () has already been executed,.then should be completed as soon as possible, Future. Value (1). Then ((value) => print('Microtask 3')); print('main 1'); }Copy the code

Print the result.

flutter: main 1
flutter: Microtask 1
flutter: Microtask 2
flutter: Microtask 3
Copy the code
test_.then()Immediate execution
Void main() {// If. Then Added to the task queue, the order is delayed -> then 1 -> Microtask -> then 2 // If. Then not added to the task queue, the task must be executed immediately. Delayed -> then 1 -> then 2 -> Microtask Future. Delayed (Duration(seconds: 1),() => print('delayed')) .then((value) { scheduleMicrotask(() => print('Microtask')); print('then 1'); }) .then((value) => print('then 2')); print('main 1'); }Copy the code

Print the result.

flutter: main 1
flutter: delayed
flutter: then 1
flutter: then 2
flutter: Microtask
Copy the code

So the conclusion is correct

Future

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

When you instantiate a Future:

  • theFutureAn instance of theDartManage the internal array;
  • Need theFutureExecuted code is pushed directly toEventGo into the ranks;
  • theThe future instanceReturn a state (= _stateIncomplete);
  • If the next synchronous code exists, execute it (non-future executable code)

As long as the Event Loop retrieves it from the queue, 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.

Some very important things to keep in mind:

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

There are six states of a Future instance:

Static const int _stateIncomplete = 0 // Flag set when no errors need to be handled. Static const int _stateIgnoreError = 1 static const int _stateIgnoreError = 1 Result. static const int _statePendingComplete = 2 Static const int _stateChained = 4 // Complete the status with the value (.then) static const int _stateValue Static const int _stateError = 16 static const int _stateError = 16Copy the code

Async

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

  • The return value of this method is a Future
  • It executes the method synchronously until the first await keyword, and then it suspends the execution of 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 forgot how Event Loop works…

Another thing to keep in mind

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

What are the results of executing method1() and method2()? :

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

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
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: Use the forEach() function to walk through arrays 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.

Method2: Everything runs in the same “block” of code, so it can be executed line by line.

Concurrent programming in Dart — Isolate

Dart is a single-threaded language. Is it impossible to do concurrent programming? No,

Dart is a single-threaded language, but concurrent programming is possible using Isolate. The official explanation for Isolate is:

Independent workers that are similar to threads but don’t share memory

The Isolate is not called threads directly, but rather something like threads.

But one way to think about it is,

In Dart, each thread is encapsulated in Isolate. Threads do not share memory, avoiding dead lock. Threads are independent, and garbage collection is efficient. Different isolates communicate with each other through messages.

The difference between Isolate and normal threads

We can see that the isolate is similar to Thread, but in fact the two are essentially different. The main difference is that the ISOLATE does not have shared memory between threads in the operating system.

The relationship is shown below

eachIsolateHave their ownEvent Loop(Event 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.

Start an Isolate

1. Use the underlying implementation

You need to establish communication and manage the newly established Isolate and its life cycle by yourself, which gives you high freedom, but it is relatively difficult to use

Since the ISOLates do not share memory, we need to find a way to establish communication between the caller and the called.

Each Isolate exposes a port called SendPort for passing messages to the other Isolate. The two ISOLates can communicate only if they know each other’s sendPort

The following code example is a two-way process, so both sides need to know sendPort to each other

  • CallerReceivePort. SendPort: the caller’s port
  • NewIsolateSendPort. SendPort: is the caller’s port
  1. Create the Isolate and establish communication
  2. Send a message
  3. Destruction of Isolate
Void main() async {//1. Create the Isolate and establish a communication. SendPort ReceivePort for retrieving the new ISOLATE callerReceivePort = ReceivePort(); Isolate newIsolate = await createAndCommunication(callerReceivePort); // New ISOLATE SendPort SendPort newIsolatePort = await callerReceivePort.first; Int result = await sendMessage(newIsolatePort, 10000000000); print(result); //3. Release Isolate disposeIsolate(newIsolate); } Future<Isolate> createAndCommunication(ReceivePort callerReceivePort) async {// Initialize new Isolate (spawn() : The first parameter is the entry method for the new ISOLATE, The second parameter is the caller's SendPort) Isolate Isolate = await Isolate the spawn (newIsolateEntry, callerReceivePort. SendPort); return isolate; } sendMessage(SendPort newIsolateSendPort, int num) Async {// Create a temporary port to receive a reply ReceivePort responsePort = ReceivePort(); SendPort newisolatesendport.send ([responsePort.sendport, num]); // Wait for a response and return responseport.first; } void disposeIsolate(Isolate newIsolate) { newIsolate.kill(priority: Isolate.immediate); newIsolate = null; } // New isolate entry (note: NewIsolateEntry (SendPort callerSendPort) Async {// A new instance of SendPort, ReceivePort newIsolateReceivePort = ReceivePort(); / / to the caller provides the isolate SendPort (note: here is communication establish complete) callerSendPort. Send (newIsolateReceivePort. SendPort); // Listen for incoming messages from the caller isolate to the new ISOLATE, and process the calculated returns // Note: Here is to Isolate the main program, and the processing of Isolate newIsolateReceivePort calculated here. Listen ((the message) {/ / processing calculation SendPort port = message [0]. int num = message[1]; int even = countEven(num); // Send the result port.send(even); }); } countEven(int num) {int count = 0; for(var i=0; i<=num; i++){ if(i%2==0){ count ++; } } return count; }Copy the code
Dart2.15 and later

Added the concept of Isolate group

Isolate The Isolate in the Isolate group shares various internal data structures that represent running programs. This makes the individual isolates in the group much more portable. Today, starting an additional ISOLATE in an existing ISOLATE is over 100 times faster than before, and the resulting ISOLATE consumes 10 to 100 times less memory, because there is no need to initialize the program structure

In Dart 2.15, the worker ISOLATE can call isolate.exit (), passing the result as an argument. The Dart runtime then passes the memory data containing the results from the worker ISOLATE to the master ISOLATE without replication, and the master ISOLATE can receive the results for a fixed amount of time. We have updated the compute() utility function in Flutter 2.8 to take advantage of isolate.exit (). If you are already using Compute (), you will automatically get these performance improvements after upgrading to Flutter 2.8.

void main() async { ReceivePort receivePort = ReceivePort(); Isolate.spawn(countTaskInBackground, receivePort.sendPort); int result = await receivePort.first; if (kDebugMode) { print('result = $result'); } } Future countTaskInBackground(SendPort sendPort) async { int result = await countEven(100000000); return Isolate.exit(sendPort, result); } Future<int> countEven(int num) async { int count = 0; for(var i=0; i<=num; i++){ if(i%2==0){ count ++; } } return count; }Copy the code
2. Compute once

You can directly pass in methods and parameters, and manage the Isolate internally. After the method is completed, the Isolate is released directly

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

Pay special attention to

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

In other words, programmatically created isolate instances do not allow platform-channel communication. There is another solution -> links

The resources

Github.com/xitu/gold-m…

www.bilibili.com/video/BV12K…

Juejin. Cn/post / 706514…

Mp.weixin.qq.com/s/03729uUAE…