Dart is known to be a single-threaded model, meaning that asynchronism requires EventLoop to be event-driven. Dart has only one main Thread, which is called ISOLATE instead of Thread. In fact, some time-consuming tasks are also encountered in Dart. It is not recommended to put the tasks into the main ISOLATE, otherwise, UI may be stuck. You need to create an independent ISOLATE to perform time-consuming tasks independently, and then send the final calculation results to the main ISOLATE through the message mechanism for UI update. Asynchrony is the foundation of the concurrent solution in Dart, which supports asynchrony within single and multiple ISOLates.

1. Why do we need isolate

When the Dart/Flutter application starts, a main thread, Root Isolate, is started and an EventLoop is run inside the Root Isolate. So all of the Dart code runs within the Isolate, which is like a small space on the machine with its own private block of memory and a single thread that runs an event loop. The ISOLATE provides an environment for Dart/Flutter application execution, including the required memory and EventLoop processing of event queues and microtask queues. Here is a diagram to understand the role of Root Isolate in the Flutter application:

2. What is ISOLATE

An Isolated Dart execution context. The main idea is that the ISOLATE is actually a context (or container) for Dart execution that is isolated. Isolate is Dart’s implementation of the Actor concurrency model. The Dart program in action is made up of one or more actors, which are essentially THE ISOLATE within Dart. The ISOLATE is an event loop with its own memory and single thread control. Isolate itself means “isolated” because internals between isolates are logically isolated, unlike Java, which has shared memory. The code in the ISOLATE is executed sequentially, and concurrency in any Dart program is the result of running multiple ISOLates. Because Dart does not have shared memory concurrency, there is no possibility of contention, so locks are not required and deadlocks are not an issue.

)

2.1 What is the Actor Concurrency model

Actors are similar to objects in OOP programming — they encapsulate state and communicate with other actors through messages. In object-oriented, we use method calls to pass information, while in actors, we send messages to pass information. An object receives a message (corresponding to a method call in OOP) and then does something based on that message. But the Actor concurrency model differs in this way:Each Actor is completely isolated (in line with the ISOLATE)They don’t share memory; At the same time, actors maintain their own private state and cannot be directly modified by other actors.Each Actor is isolated from each other so they communicate by sending messages. In the Actor model, each worker is called an Actor. Actors can send and process messages directly and asynchronously.

2.2 Actor concurrent communication message model

In fact, there is another concept hidden in the more specific concurrent message model of actors, that is, the concept of mailbox. In other words, messages cannot be sent and received directly between actors, but are sent to a mailbox. In fact, in the implementation of Actor, there is a corresponding implementation of Mailbox inside an Actor, which is used for sending and receiving messages.

  • For example, if ActorA wants to communicate with ActorB, it must send a “message” to ActorB by sending a message. The address should be filled in the mailbox address of ActorB, and whether ActorB receives the “message” should be decided by ActorB himself.
  • Each Actor has its own Mailbox. Any Actor can send “message” according to the mailbox address of the corresponding Actor. “Message” delivery and reading are two processes, so the interaction between actors is completely decoupled.

2.2 Characteristics of Actor concurrency model

  • Calculations can be done inside actors without consuming the caller’s CPU time slice, and even the concurrency strategy is self-determining
  • Actors communicate with each other by sending asynchronous messages
  • State can be saved and modified inside actors

2.3 Rules of Actor concurrency model

  • All calculations are performed in actors
  • Actors can only communicate with each other through messages
  • When an Actor receives a message, it can do three things: send the message to other actors, create other actors, accept and process the message, and modify its own state

2.4 Advantages of Actor concurrency model

  • Actors can be updated and upgraded independently because each Actor is a relatively independent entity, independent of each other and does not affect each other
  • Actors can support both local and remote invocation, because actors essentially use message-based communication mechanisms. Whether interacting with local actors or remote actors, they communicate through messages
  • Actors have good error handling and don’t need to worry about message timeouts or waits because messages are sent asynchronously between actors
  • Actor is also highly efficient and scalable, because it supports local call and remote call. When local Actor processing capacity is limited, remote Actor can be used for collaborative processing

2.5 ISOLATE Concurrency Model Features

The isolate is conceptually Thread threads, but the only difference is that the isolate is different from Thread threadsMultiple ISOLates are isolated from each other and do not share memory space. Each ISOLATE has its own memory space, thus avoiding lock contention. Since each isolate is an isolation, the communication between them isBased on the Actor concurrency model above send asynchronous messages to achieve communicationSo it’s more intuitive to think of an ISOLATE as an Actor in the concurrency model. There’s more in isolatePortThe concept of “Send Port” is divided into “Receive Port” and “Send Port”. It can be understood as the realization of mailbox inside each Actor in the Actor model, which can manage Message well.

3. How to use ISOLATE

3.1 Introduction to the ISOLATE Package

To perform concurrent operations using the ISOLATE, import the ISOLATE

import 'dart:isolate';
Copy the code

The Library contains the following:

  • IsolateClass: Isolated context for Dart code execution
  • ReceivePortClass: it is a receiving messageStream , ReceivePortCan be generatedSendPort, as can be seen from the Actor model rules aboveReceivePortTo receive messages, you can send messages to othersisolateSo to send a message you need to generateSendPortAnd then fromSendPortSent to the corresponding ISOLATEReceivePort .
  • SendPortClass: Sends messages to the ISOLATE, or to be exact, to the ISOLATEReceivePort 

Spawn is a static method that returns a Future< ISOLATE >. There are two required parameters. The entryPoint function and the argument message, where the **entryPoint function must be a top-level function (a concept mentioned in previous articles) or a static method, and the argument message must contain SendPort. **

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

3.2 Creating and Starting the ISOLATE

Sometimes, you need to create and start an ISOLATE in different ways based on specific scenarios.

Create a NewIsolate and establish a handshake connection between RootIsolate and NewIsolate

As described earlier,isolateThey do not share any memory space and communicate with each other via asynchronous messages. So you need to find a wayrootIsolateAnd newly creatednewIsolateA means of communication established between. eachisolateA Port is exposed to theisolateThe port that sends the message is calledSendPort.and that means making it happenrootIsolateAnd newly creatednewIsolateTo communicate, you must know each other’s ports.

// Implement newIsolate to establish a connection with rootIsolate(default)
import 'dart:isolate';

// Define a newIsolate
late Isolate newIsolate;
// Define a newIsolateSendPort that needs to be held by rootIsolate,
// In rootIsolate, newIsolateSendPort is used to send messages to newIsolate
late SendPort newIsolateSendPort;

void main() {
  establishConn(); // Establish a connection
}

// In particular, establishConn implements rootIsolate
void establishConn() async {
  // Step 1: The default execution environment is rootIsolate, so create a rootIsolateReceivePort
  ReceivePort rootIsolateReceivePort = ReceivePort();
  // Step 2: Get rootIsolateSendPort
  SendPort rootIsolateSendPort = rootIsolateReceivePort.sendPort;
  // Step 3: Create an instance of newIsolate and pass rootIsolateSendPort as a parameter to newIsolate, This is so that rootIsolateSendPort is held in newIsolate so that messages can be sent to rootIsolate from newIsolate
  newIsolate = await Isolate.spawn(createNewIsolateContext, rootIsolateSendPort); // Note that the createNewIsolateContext callback is executed under newIsolate, and rootIsolateSendPort is the argument to the createNewIsolateContext callback
  // Step 7: A message from newIsolate was received via rootIsolateReceivePort, so notice that it is await because it is an asynchronous message
  // Only the received message is newIsolateSendPort, which is assigned to the global newIsolateSendPort, so rootIsolate holds newIsolateSendPort
  var messageList = await rootIsolateReceivePort.first;
  // Step 8, the connection was successfully established
  print(messageList[0] as String);
  newIsolateSendPort = messageList[1] as SendPort;
}

// Note that the createNewIsolateContext execution environment is newIsolate
void createNewIsolateContext(SendPort rootIsolateSendPort) async {
  // Step 4: Note that the callback environment becomes newIsolate, so a newIsolateReceivePort is created
  ReceivePort newIsolateReceivePort = ReceivePort();
  NewIsolateSendPort = newIsolateSendPort = newIsolateSendPort = newIsolateSendPort = newIsolateSendPort = newIsolateSendPort
  SendPort newIsolateSendPort = newIsolateReceivePort.sendPort;
  // Step 6: In particular, this is where rootIsolateSendPort is used to send messages to rootIsolate, but the message is SendPort for newIsolate so rootIsolate can get the SendPort for newIsolate
  rootIsolateSendPort.send(['connect success from new isolate', newIsolateSendPort]);
}
Copy the code

Output result:

3.2 ISOLATE Another encapsulation method compute

The compute method is an implementation of the ISOLATE that has been encapsulated in the Flutter SDK. Note that this method is only defined in the Flutter SDK and not in the Dart SDK. Because the Isolate in DART is heavyweight and the transfer of UI threads and data within the Isolate is complex, Flutter encapsulates a lightweight Compute operation in the Foundation library to simplify user code. This method is usually used to run a long piece of code and then use the encapsulated method without interacting with the ISOLATE. The source code is located in flutter/foundation/_isolates_io.dart.

Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String debugLabel }) async {
  if(! kReleaseMode) { debugLabel ?? = callback.toString(); }final Flow flow = Flow.begin();
  Timeline.startSync('$debugLabel: start', flow: flow);
  final ReceivePort resultPort = ReceivePort();
  final ReceivePort errorPort = ReceivePort();
  Timeline.finishSync();
  final Isolate isolate = await Isolate.spawn<_IsolateConfiguration<Q, FutureOr<R>>>(
    _spawn,
    _IsolateConfiguration<Q, FutureOr<R>>(
      callback,
      message,
      resultPort.sendPort,
      debugLabel,
      flow.id,
    ),
    errorsAreFatal: true,
    onExit: resultPort.sendPort,
    onError: errorPort.sendPort,
  );
  final Completer<R> result = Completer<R>();
  errorPort.listen((dynamic errorData) {
    assert(errorData is List<dynamic>);
    assert(errorData.length == 2);
    final Exception exception = Exception(errorData[0]);
    final StackTrace stack = StackTrace.fromString(errorData[1] as String);
    if (result.isCompleted) {
      Zone.current.handleUncaughtError(exception, stack);
    } else{ result.completeError(exception, stack); }}); resultPort.listen((dynamic resultData) {
    assert(resultData == null || resultData is R);
    if(! result.isCompleted) result.complete(resultDataas R);
  });
  await result.future;
  Timeline.startSync('$debugLabel: end', flow: Flow.end(flow.id));
  resultPort.close();
  errorPort.close();
  isolate.kill();
  Timeline.finishSync();
  return result.future;
}
Copy the code

3.3 Limitations of the ISOLATE

Note that platform-channel communication is only supported within the main Isolate, which is created when the Application is started. In other words, platform-channel communication could not run on our custom isolates-created interface.

4. The ISOLATE communicates with each other

The two isolates need to communicate first by getting each other’s in their respective domainsSendPortIn this way, the first step is to complete the handshake between the two parties, and then the current sendPort needs to be brought again when sending the message, and finally the message can be sent to each other to realize communication. Continue with the above example of successfully establishing a connection:

// Implement communication between newIsolate and rootIsolate

import 'dart:io';
import 'dart:isolate';

// Define a newIsolate
late Isolate newIsolate;
// Define a newIsolateSendPort that needs to be held by rootIsolate,
// In rootIsolate, newIsolateSendPort is used to send messages to newIsolate
late SendPort newIsolateSendPort;

void main() {
  establishConn(); // Establish a connection
}

// In particular, establishConn implements rootIsolate
void establishConn() async {
  // Step 1: The default execution environment is rootIsolate, so create a rootIsolateReceivePort
  ReceivePort rootIsolateReceivePort = ReceivePort();
  // Step 2: Get rootIsolateSendPort
  SendPort rootIsolateSendPort = rootIsolateReceivePort.sendPort;
  // Step 3: Create an instance of newIsolate and pass rootIsolateSendPort as a parameter to newIsolate, This is so that rootIsolateSendPort is held in newIsolate so that messages can be sent to rootIsolate from newIsolate
  newIsolate = await Isolate.spawn(createNewIsolateContext, rootIsolateSendPort); // Note that the createNewIsolateContext callback is executed under newIsolate, and rootIsolateSendPort is the argument to the createNewIsolateContext callback
  // Step 7: A message from newIsolate was received via rootIsolateReceivePort, so notice that it is await because it is an asynchronous message
  // Only the received message is newIsolateSendPort, which is assigned to the global newIsolateSendPort, so rootIsolate holds newIsolateSendPort
  newIsolateSendPort = await rootIsolateReceivePort.first;
  // Step 8: After the connection is established, messages can be sent to newIsolate under rootIsolate
  sendMessageToNewIsolate(newIsolateSendPort);
}

// The sendMessageToNewIsolate execution environment is rootIsolate
void sendMessageToNewIsolate(SendPort newIsolateSendPort) async {
  ReceivePort rootIsolateReceivePort = ReceivePort(); // Create a specialized reply message rootIsolateReceivePort
  SendPort rootIsolateSendPort = rootIsolateReceivePort.sendPort;
  newIsolateSendPort.send(['this is from root isolate: hello new isolate! ', rootIsolateSendPort]);// Note: In order to receive newIsolate reply messages, rootIsolateSendPort is required
  // Step 11: Listen for messages from newIsolate
  print(await rootIsolateReceivePort.first);
}

// Note that the createNewIsolateContext execution environment is newIsolate
void createNewIsolateContext(SendPort rootIsolateSendPort) async {
  // Step 4: Note that the callback environment becomes newIsolate, so a newIsolateReceivePort is created
  ReceivePort newIsolateReceivePort = ReceivePort();
  NewIsolateSendPort = newIsolateSendPort = newIsolateSendPort = newIsolateSendPort = newIsolateSendPort = newIsolateSendPort
  SendPort newIsolateSendPort = newIsolateReceivePort.sendPort;
  // Step 6: In particular, this is where rootIsolateSendPort is used to send messages to rootIsolate, but the message is SendPort for newIsolate so rootIsolate can get the SendPort for newIsolate
  rootIsolateSendPort.send(newIsolateSendPort);
  // Step 9: newIsolateReceivePort listens to receive messages from rootIsolate
  receiveMsgFromRootIsolate(newIsolateReceivePort);
}

/ / note: need special receiveMsgFromRootIsolate is newIsolate execution environment
void receiveMsgFromRootIsolate(ReceivePort newIsolateReceivePort) async {
  var messageList = (await newIsolateReceivePort.first) as List;
  print('${messageList[0] as String}');
  final messageSendPort = messageList[1] as SendPort;
  // Step 10: Immediately after receiving the message, send a reply message to rootIsolate
  messageSendPort.send('this is reply from new isolate: hello root isolate! ');
}
Copy the code

Running results:

5. Differences between ISOLATE and common Threads

The differences between ISOLATE and common threads need to be distinguished from different dimensions:

  • 1. From the bottom operating system dimension

Osthreads are created on the ISOLATE and Thread levels, which are the same. Osthreads are created on the OSThread level.

  • 2, from the role of the dimension

All to provide a runtime environment for the application.

  • 3. From the dimension of implementation mechanism

The main difference between the ISOLATE and threads is that threads share memory in most cases, causing resource competition. However, the ISOLATE does not share memory with each other.

6. In which scenario should Future or ISOLATE be used

In fact, this problem is worth paying attention to, because it is directly related to actual development. Sometimes you really need to know when to use Future and when to use ISOLATE. Some people say that using ISOLATE is heavy and generally not recommended. There are also some use scenarios for THE ISOLATE. Some people may wonder why it takes time to make network requests. Usually, Future is enough, why not use the ISOLATE. Here are the answers.

  • If a piece of code is not going to break, then just use normal synchronous execution.
  • It is recommended if the code snippet can run independently without affecting the smoothness of the applicationFuture 
  • It is recommended if heavy processing may take some time to complete and will affect the smoothness of the applicationisolate 

In other words, it is recommended to use futures as much as possible (directly or indirectly through asynchronous methods), because the code for those Futures will run once the EventLoop has an idle period. This may seem a bit abstract, but here is a code runtime metric to measure options:

  • It is recommended if a method takes a few millisecondsFuture .
  • It is recommended if a process may take several hundred millisecondsisolate .

Here are some specific scenarios for using ISOLATE:

  • 1, JSON parsing: decoding JSON, this is the result of HttpRequest, may take some time, can use the packageisolate 的 computeTop-level methods.
  • 2. Encryption and decryption: the encryption and decryption process is time-consuming
  • 3, picture processing: for example, cutting pictures is time-consuming
  • Load the big picture from the network

7. Summary from Mr. Xiong Meow

This is the end of the introduction of the content related to THE ISOLATE in Dart asynchronous programming. This article has a comprehensive analysis of the ISOLATE, including why the ISOLATE is needed, how to use the ISOLATE, the use scenario of the ISOLATE, and finally the source code analysis of root ISOLATE.

Thank you for your attention, Mr. Xiong Meow is willing to grow up with you on the technical road!