Dart is based on Isolate

background

In other languages, in order to make efficient use of multi-core CPU, multi-thread parallelism is usually used to realize concurrent code execution and ensure the cooperation between multi-threads by sharing data. However, this mode gives rise to many problems, such as resource consumption caused by thread opening and deadlock of data sharing agent.

Whether APP or Web, the CPU is idle most of the time and generally does not require intensive and concurrent processing. Dart, as a front-end development and design language, does not use multithreading in concurrency design. Instead, it uses the single-thread model of Isolate (Isolation) to solve the dependency of concurrent tasks on multithreading.

An Isolate

Each Isolate consists of the following components:

(1) Stack is used to store function call context and call link information.

(2) Heap is used to store objects, and Heap memory reclamation management is similar to Java.

(3) Queues for storing asynchronous callbacks are divided into MicroTaskQueue and EventQueue.

(4) and an EventLoop for handling asynchronous callbacks.

Isolate Running code

Code written in DART falls into two types:

Synchronous code: normally written code.

Asynchronous code: Some functions that return types Future and Stream.

Because THE Isolate is a single-threaded model, when code runs it encounters asynchronous code, it is thrown into a Queue and synchronous code is only run sequentially. Asynchronous tasks are executed sequentially only when the synchronous code is complete.

Let’s write a demo to verify this. Future.delayed is the asynchronous code shown below.

Future printc(){
  Future.delayed(new Duration(seconds: 2), () {print("a");// Callback for asynchronous code is executed after all synchronous code is executed
  });
  Future.delayed(new Duration(seconds: 1), () {print("b");// Callback for asynchronous code is executed after all synchronous code is executed
  });
  print("c");
  var start = DateTime.now();
  for(int i=0; i<100000; i++){for(int j=0; j<100000;j++){
      i*j+j*i-j*i;
    }
  }
  var end = DateTime.now();
  print("Synchronizing time-consuming tasks:${end.second-start.second}SEC. "");
  print("d");
}
Copy the code

The execution result is as follows:

C Synchronization task :11 seconds D B ACopy the code

To explain the result, asynchronous tasks that print a and B are inserted into the Queue and their callbacks are not currently executed. Run the synchronization code, so c is printed first, and D is printed after the synchronization task is completed. After the synchronous code execution is complete, EventLoop extracts the asynchronous code execution from the Queue. Although the asynchronous task A comes first, it has a long delay, so b is printed first.

Synchronous waiting: Use the await keyword when calling asynchronous code (such as Future) and the async keyword in method declarations.

If you want to run the asynchronous task before executing the synchronous code, you can use await to run the asynchronous task and block the synchronous code. After the asynchronous task is complete, the synchronous code continues to be executed.

Future printc() async{
  await Future.delayed(new Duration(seconds: 2), () {print("a");// Callback for asynchronous code is executed after all synchronous code is executed
  });
  await Future.delayed(new Duration(seconds: 1), () {print("b");// Callback for asynchronous code is executed after all synchronous code is executed
  });
  print("c");
  var start = DateTime.now();
  for(int i=0; i<100000; i++){for(int j=0; j<10000;j++){
      i*j+j*i-j*i;
    }
  }
  var end = DateTime.now();
  print("Synchronizing time-consuming tasks:${end.difference(start).inSeconds}SEC. "");
  print("d");
}
Copy the code

Adding the await modifier to the two asynchronous tasks will execute sequentially as if it were synchronous code.

A B C Synchronization time :1 second DCopy the code

EventLoop

Asynchronous tasks are dropped to the EventQueue and executed by the EventLoop. Asynchronous tasks are constantly fetched from the time queue and run.

The following figure shows how EventLoop takes the task from the queue and executes it.

The MicrotaskQueue in the figure is the MicrotaskQueue with the highest priority. Each loop checks the MicrotaskQueue first. If there are any microtasks, the microtasks will be executed first.

Microtasks: Microtasks are usually used to perform very short asynchronous operations and cannot be too large. There are ways to generate microtasks

/ / way
scheduleMicrotask((){
  print("I'm a micromission!");
});
2 / / way
Future.microtask(() => print("I'm another microtask!"));
Copy the code

Events: Events, mainly from Future, including IO, gestures, draws, timers, messages that communicate with Isloate, etc.

Note the special scenarios used by the Future here.

Future.delayed(new Duration(seconds: 2), () {print("a");// Callback for asynchronous code is executed after all synchronous code is executed
});
Copy the code

Future.delayed Asynchronous callbacks are added to the end of the EventQueue only after the delay is over, not immediately, and execution time is not guaranteed.

  Future(()=>print("zzz"))
  .then((value) => print("xxx"))
  .then((value) => print("yyy"))
  .then((value) => print("www"));
Copy the code

Future.then complements callbacks to asynchronous events, and then does not add events to the EventQueue, but executes immediately after the previous Future execution completes. The order of execution of multiple THEN internal tasks can be guaranteed.

  Future aaa = Future(()=>print("aaa"));
// Aaa is in the finished state
  aaa.then((value) => print("bbb"));
// Future(()=>null) generates a completed Future
  Future(()=>null).then((value) => print("ccc"));
Copy the code

For a Future that has already been completed, calling then is not executed immediately, but instead adds the callback from then to the Microtask queue.

Case Study:

void dartLoopTest() {
  Future x0 = Future(() => null);
  Future x = Future(() => print('1'));
  Future(() => print('2'));
  scheduleMicrotask(() => print('3'));
  x.then((value) {
    print('4');
    Future(() => print('5'));
  }).then((value) => print('6'));
  print('7');
  x0.then((zvalue) {
    print('8');
    scheduleMicrotask(() {
      print('9');
    });
  }).then((value) => print('10'));
}
Copy the code

The output is:

7 
3
8
10
9
1
4
6
2
5
Copy the code
  1. Print (‘7’);

  2. ScheduleMicrotask (() => print(‘3’));

  3. The current MicroTask queue is empty, events are fetched from the Event queue and executed in code order

    Future x0 = Future(() => null);
    Copy the code

    There is no output at this point, but X0 is the Future of the completed state, and the first then callback is added to the MicroTask queue.

    x0.then((zvalue) {
        print('8');
        scheduleMicrotask(() {
          print('9');
        });
      }).then((value) => print('10'));
    Copy the code

    Then run the microtask, calling print(‘8’); , perform scheduleMicrotask (() {print (‘ 9 ‘); }); Add print(‘9′) to the microtask queue; The task. Print (’10’) is called first because the current microtask is not finished.

  4. Future x = Future(() => print(‘1’)) After that, x is also in the completed state, and the then callback is added to the microtask queue for execution. Print 4 in the microtask, add an event that prints 5 to the event queue, and finally print 6.

      x.then((value) {
        print('4');
        Future(() => print('5'));
      }).then((value) => print('6'));
    Copy the code
  5. There are also events that print 2 and print 5 in the event queue, which are executed in order to output the final result.

void testThenAwait(){
  Future(()=>print("AAA"))
      .then((value) async= >await Future(()=>print("BBB")))
      .then((value) => print("CCC"));
  Future(()=>print("DDD"));
}
Copy the code

When we use await in THEN we block the current THEN call down.

Output result:

AAA
DDD
BBB
CCC
Copy the code

Output analysis:

  1. First, there are two Futures in this method, which are added to the Event queue in sequence.
  2. Run the first Future, print AAA,
  3. The first THEN is executed synchronously, adding a third Future to the Event queueFuture(()=>print("BBB")Must wait due to synchronous waitFuture(()=>print("BBB")The command can be continued only after the execution is complete.
  4. Run the first asynchronous Event in the Event queueFuture(()=>print("DDD"));Print DDD,
  5. Run the remaining asynchronous events in the Event queueFuture(()=>print("BBB"), print the BBB
  6. Then is executed to print CCC

Create the Isolate

Basic method

The code in Flutter applications runs in root isloate by default. Although single-threaded, it is sufficient to handle all kinds of asynchronous tasks.

When there are computationally intensive time-consuming tasks, a new Isolate needs to be created to perform time-consuming calculations to avoid blocking root isloate. Due to memory isolation between different isolates, communication is implemented through ReceivePort and SendPort.

Use isolate. spawn to create a new Isolate. Look at the function signature

  external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused = false.bool errorsAreFatal = true,
      SendPort? onExit,
      SendPort? onError,
      @Since("2.3") String? debugName});
Copy the code

The external modifier indicates that this method has different implementations on different platforms, somewhat similar to Java’s native method.

Future

indicates that it is an asynchronous method. In order to get the Isolate created in the synchronous code, you need to add synchronization and await the call.

EntryPoint defines the function signature (empty return value, with one and only input parameter) that can accept the computation task, which must be either a top-level function or a static method.

T message is the parameter required to run computing tasks in the new Isolate. The type must be the same as that accepted by entryPoint.

So the two main steps to create a new Isolate are as follows:

  1. Use top-level functions or static methods to define computational tasks

    // Time calculation part
    int fibonacci(int n) {
      return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
    }
    // 1. Compute tasks
    void task1(int start) {
      DateTime startTime = DateTime.now();
      int result = fibonacci(start);
      DateTime endTime = DateTime.now();
      print("Calculation time:${endTime.difference(startTime)}Results:${result.toString()}");
    }
    void main() {
      task1(50);
    }
    // Output: Calculation time: 0:00:48.608656 Result: 12586269025
    Copy the code

The calculations above took 48 seconds and had to be done on the new Isolate

  1. To prepare for the entry, call spawn to create a new Isolate.

    void main() async {
      Isolate newIsolate = await Isolate.spawn(task1,10,debugName: "isolateDebug");
      print("The end!);
    }
    Copy the code

    Output result:

    The end!Copy the code

    The results calculated on the new Isolate are not printed to the current Console. In this case, the current Isolate and SendPort are used to establish a connection between the current and new Isolate.

One-way communication

Define time-consuming tasks that can receive SendPort from the host, and send the results back to the host Isolate through Send after calculation.

/// [hostSendPort] Is used to send results to the host ISOLATE
void task2(SendPort hostSendPort){
      DateTime startTime = DateTime.now();
      int result = fibonacci(47);
      DateTime endTime = DateTime.now();
      var state = "Calculation time:${endTime.difference(startTime)}Results:${result .toString()}";
      hostSendPort.send(state);
}
Copy the code

In the host Isolate, you define a ReceivePort for receiving results, which is also a Stream, and you can set up a listener to handle the received results.

void main() async {
  // Define the ReceivePort from which the host receives the result, and set up the listener.
  ReceivePort hostReceivePort = ReceivePort();
  hostReceivePort.listen((message) {
    print(message);
  });
  // Define hostSendPort that sends data to hostReceivePort
  SendPort hostSendPort = hostReceivePort.sendPort;
  // hostSendPort.send("message"); We tested the situation where results were sent on the current ISOLATE.
  Isolate newIsolate =
      await Isolate.spawn(task2, hostSendPort);
}
Copy the code

Calculation results:

Calculation time: 0:00:11.959955 Result: 2971215073Copy the code

Two-way communication

After the above modification, SendPort from the host is sent to the sub-ISOLATE, and the result is calculated and sent back to the host Isolate. The host Isolate processes the result through ReceivePort. Then the child Isolate sends data to the host Isolate.

But how does the host Isolate send data to the child Isolate?

The subisolate sends data to the host Isolate by holding SendPort in the host. In order for the host Isolate to send data to the sub-isolate, the host also needs to hold SendPort on the sub-isolate. The simplest solution is to create subSendPort in the subISOLATE and pass it back to the host.

void task3(SendPort hostSendPort) {
  ///5. Create a ReceivePort of the sub-ISOLATE to receive initialization parameters sent from the host
  ReceivePort subReceivePort = ReceivePort();
  subReceivePort.listen((start) {
    if (start is int) {
      ///9.Computes initialization parameters received from the host.
      DateTime startTime = DateTime.now();
      int result = fibonacci(start);
      DateTime endTime = DateTime.now();
      var state =
          "Calculation time:${endTime.difference(startTime)}Results:${result.toString()}";

      ///10. After the calculation, send the result through the host hostSendPort.hostSendPort.send(state); }});///6. Send sendPort of the subisolate to the host for the host to send initialization parameters to the subisolate.
  hostSendPort.send(subReceivePort.sendPort);
}

void main() async {
  ///1 Defines the port from which the host receives the results and the port from which the parameters are sent
  ReceivePort hostReceivePort = ReceivePort();

  ///2 Define the SendPort reference of the subISOLATE
  SendPort subSendPort;

  ///3 Define a listener. The part of the listener will not run for the time being
  hostReceivePort.listen((message) {
    if (message is SendPort) {
      /// 7. The SendPort of the subISOLATE was received, and the bidirectional communication configuration was complete
      subSendPort = message;

      /// 8. Send initialization data of computing tasks to the subisolate
      subSendPort.send(2);
      subSendPort.send(10);
      subSendPort.send(20);
      subSendPort.send(30);
      subSendPort.send(40);
      subSendPort.send(48);
    } else if (message is String) {
      /// 11. Print the calculated results in Isolate.
      print(message);
    } else {
      print("Data received does not conform to specification"); }});///4 Define hostSendPort that sends data to hostReceivePort and create the hostReceivePort
  SendPort hostSendPort = hostReceivePort.sendPort;
  Isolate newIsolate = await Isolate.spawn(task3, hostSendPort);
}
Copy the code

The final output is:

Calculation Time: 0:00:00.000000 Result: 1 Calculation time: 0:00:00.000000 Result: 55 Calculation time: 0:00:00.000000 Result: 6765 Calculation time: 0:00:00.002999 Result: 832040 Calculation time: 0:00:00.393017 Result: 102334155 Calculation time: 0:00:19.179601 Result: 4807526976Copy the code

The above code may seem a bit convoluted in order, but comments have been added to describe the basic running order. It can be seen that to complete a basic two-way communication function, to write a large section of configuration code, really belongs to the business part of the code is a few lines, it is very cumbersome to use.

Simplify the use

The use of Isolate in Flutter is simplified, and bidirectional communication can be easily realized by Compute method.

import 'package:flutter/foundation.dart';
int fibonacci(int n) {
  return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}
void main()async{
    var result = await compute( fibonacci,20);
    print(The calculated results are as follows:$result");
}
Copy the code

Output result:

I/ FLUTTER (9899) is calculated to be 6765Copy the code

Reference:

Blog.csdn.net/weixin_3387…

Sg. Jianshu. IO/p/a4a871995…