The preface

On the last day of 2021, Dart version 2.15 has been officially released. Dart has improved a lot of content, and today we are going to focus on the ISOLATE worker. Official tweet link

Before exploring new changes, let’s review the use of ISOLATE.

Isolate the role of

Q: Flutter is developed using coroutines based on single-threaded mode. Why does it need ISOLATE?

First we need to clarify the difference between isolate and future. Let’s use a simple example to illustrate. The Demo is a simple page with a revolving progress and a button in the middle that triggers a time-consuming method.

///Counting the number of even numbers (specific time consuming operations) is used in the following example code
static int calculateEvenCount(int num) {
    int count = 0;
    while (num > 0) {
      if (num % 2= =0) {
        count++;
      }
      num-; }return count;
  }
Copy the code
///Button click event
onPressed: () {
      // Trigger a time-consuming operation
      doMockTimeConsume();
  }
Copy the code
  • Method 1: we will use the time-consuming operationfutureTo encapsulate
///Encapsulate time-consuming operations with a future
static Future<int> futureCountEven(int num) async {
    var result = calculateEvenCount(num);
    return Future.value(result);
  }

///Time-consuming events
void doMockTimeConsume() async {
    var result = await futureCountEven(1000000000);
    _count = result;
    setState(() {});
  }
Copy the code

The results are as follows:

Conclusion: UsefutureSince a single thread is still working, asynchrony is just a concurrent operation in the same thread that still blocks UI refreshes.

  • Method 2: UseisolateCreate a new thread that avoids the main thread and doesn’t interfere with UI refresh
// Simulate time-consuming operations
  void doMockTimeConsume() async {
    var result = await isolateCountEven(1000000000);
    _count = result;
    setState(() {});
  }
Copy the code
  ///Use the ISOLATE to encapsulate time-consuming operations
  static Future<dynamic> isolateCountEven(int num) async {
    final p = ReceivePort();
    ///Send the parameters
    await Isolate.spawn(_entryPoint, [p.sendPort, num]);
    return (await p.first) as int;
  }

 static void _entryPoint(SendPort port) {
    SendPort responsePort = args[0];
    int num = args[1];
    ///Receives parameters, performs time-consuming operations and returns data
    responsePort.send(calculateEvenCount(num));
  }
Copy the code

The results are as follows:

Conclusion: UseisolateMulti-thread parallelism is implemented so that time-consuming operations in a new thread will not interfere with the REFRESH of the UI thread.

The limitations of ISOLATE, why need to optimize?

Iso has two important limitations.

  • The isolate consumes a large amount of space. Each creation takes at least 2Mb space and risks OOM.
  • The memory space between the ISOLates is independent. When parameters or results are transmitted across isOs, a deep copy is required, which takes time and may cause UI congestion.

Isolate the new features

Dart 2.15 update adds the concept of group to ISO, and the working characteristics of ISOLATE group can be summarized as follows:

  • Isolate The Isolate in the group shares various internal data structures
  • Isolate groupStill stopMutable object access was shared between the ISOLATE, but since the ISOLATE groups were implemented using the shared heap, this gave them more functionality.

The official tweet gave an example:

The worker ISOLATE takes the data through network calls, parses it into a large JSON object graph, and returns this JSON graph to the main ISOLATE.

Dart before 2.15: Deep copy is required to perform this operation, and if the copy takes longer than the frame budget time, the interface will stall.

Dart 2.15: Worker ISOLATE allows you to 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.

Important: The isolate.exit () method is provided to pass memory data containing the result from the worker to the master Isolate without replication.

Note: To use the new Dart features, upgrade the FLUTTER SDK to link 2.8.0 above.

In ISOLATE: The difference between exit and send and their usage

After Dart updates, there are two ways to transfer data back from the worker isolate (child threads) to the main isolate (main thread).

  • Method 1: Usesend
responsePort.send(data);
Copy the code

Click on the send method to view the source code comment and see the following sentence:

Conclusion: Send itself does not block and is sent immediately, but may require a linear time cost to replicate data.

  • Method 2: Useexit
Isolate.exit(responsePort, data);
Copy the code

The official website explains as follows:

Conclusion: Message passing between quarantines typically involves data replication, so it can be slow and increases with message size. But exit(), which is the memory that holds messages in exit isolation, is not copied, but transmitted to the main ISOLATE. The transfer is fast and takes place in constant time.

The _entryPoint method is optimized as follows:

 static void _entryPoint(SendPort port) {
    SendPort responsePort = args[0];
    int num = args[1];
    ///Receives parameters, performs time-consuming operations and returns data
    //responsePort.send(calculateEvenCount(num));
    Isolate.exit(responsePort, calculateEvenCount(num));
  }
Copy the code

Summary: Useexit()alternativeSendPort.sendTo avoid data replication and save time.

Isolate group

How do I create an ISOLATE Group? The official explanation is as follows:

When an isolate calls Isolate.spawn(), the two isolates have the same executable code and are in the same isolate group. Isolate groups enable performance optimizations such as sharing code; a new isolate immediately runs the code owned by the isolate group. Also, Isolate.exit() works only when the isolates are in the same isolate group.

When another ISOLATE is called within an ISOLATE, the two isolations have the same executable code and are in the same isolation group.

PS: Xiaohong has not thought of specific use scenarios for the time being, so let’s put them aside.

Practice: How does the ISOLATE handle continuous data

Combined with the time-consuming method calculateEvenCount above, the ISOLATE handles continuous data in conjunction with the stream stream design. Specific demo is as follows:

///Test the entrance
static testContinuityIso() async {
    final numbs = [10000.20000.30000.40000];
    await for (final data in_sendAndReceive(numbs)) { log(data.toString()); }}Copy the code
///Concrete ISO implementation (main thread)
static Stream<Map<String.dynamic>> _sendAndReceive(List<int> numbs) async* {
    final p = ReceivePort();
    await Isolate.spawn(_entry, p.sendPort);
    final events = StreamQueue<dynamic>(p);

    // Get SendPort from the subisolate to send data
    SendPort sendPort = await events.next;
    for (var num in numbs) {
      // Send a data, wait for a data result, and repeat
      sendPort.send(num);
      Map<String.dynamic> message = await events.next;
      // Each time the result is exposed through the stream
      yield message;
    }
    // Send null as the end identifier
    sendPort.send(null);
    await events.cancel();
  }
Copy the code
///Concrete ISO implementation (child threads)
static Future<void> _entry(SendPort p) async {
    final commandPort = ReceivePort();
    // Send a sendPort to the master ISO for the master ISO to send parameters to the child ISO
    p.send(commandPort.sendPort);
    await for (final message in commandPort) {
      if (message is int) {
        final data = calculateEvenCount(message);
        p.send(data);
      } else if (message == null) {
        break; }}}Copy the code

This is just an idea ~