Dart supports concurrent code programming with async-await, ISOLATE, and some asynchronous type concepts such as Future and Stream. This article will give a brief introduction to async-await, Future and Stream, with emphasis on ISOLATE.

In the application, all the Dart code runs on the ISOLATE. Each Dart ISOLATE has a separate running thread that cannot share mutable objects with the other ISOLATES. In situations where communication is required, the ISOLATE uses messaging. Although Dart’s ISOLATE model design is based on more low-level primitives such as processes and threads provided by the operating system, we do not discuss its implementation in this article.

Most Dart applications use only one ISOLATE (the main ISOLATE), but you can create many more to execute code in parallel across multiple processor cores.

Be careful when using multiple platforms

All Dart applications can use async-await, Future, and Stream. The ISOLATE is only implemented for native platform use. Web applications built using Dart can use Web Workers to achieve similar functionality.

Asynchronous types and syntax

If you are already familiar with Future, Stream and async-await, skip to the ISOLATE section and read about it.

Future and Stream types

The Dart language and libraries provide functionality through Future and Stream objects that will return values in the Future of the current call. Using the JavaScript Promise as an example, a Promise in Dart that eventually returns a value of type int should be declared Future

; A promise that consistently returns a series of int values should be declared Stream

.

Let’s use Dart: IO for another example. The synchronous method of File, readAsStringSync(), reads the File as a synchronous call, blocking until the read is complete or an error is thrown. This will either return a String or throw an exception. Its asynchronous equivalent, readAsString(), returns an object of type Future

immediately when called. At some point in the Future, the Future

will end and return a String or an error.

Why does it matter whether a method is synchronous or asynchronous? Because most applications need to do many things at the same time. For example, an application might make an HTTP request and make different interface updates to the user’s actions before the request returns. Asynchronous code helps keep the application in a more interactive state.

Async and await the syntax

The async and await keywords are declarations used to define asynchronous functions and get their results.

Here is an example of synchronous code blocking when calling file I/O:

void main() {
  // Read some data.
  final fileData = _readFileSync();
  final jsonData = jsonDecode(fileData);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

String _readFileSync() {
  final file = File(filename);
  final contents = file.readAsStringSync();
  return contents.trim();
}
Copy the code

Here is similar code, but with an asynchronous call:

void main() async {
  // Read some data.
  final fileData = await _readFileAsync();
  final jsonData = jsonDecode(fileData);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

Future<String> _readFileAsync() async {
  final file = File(filename);
  final contents = await file.readAsString();
  return contents.trim();
}
Copy the code

The main() function uses the await keyword before calling _readFileAsync() so that other Dart code (such as event handlers) can continue to execute while the native code (file I/O) executes. With await, the Future

type returned by the _readFileAsync() call is also converted to String. This implicitly converts the resulting content to String when it is assigned to a variable.

The await keyword is valid only in functions where async is defined before the function body.

As shown in the figure below, Dart code is paused when readAsString() executes non-DART code, both in the Dart VM and on the system. After readAsString() returns the value, the Dart code continues execution.

If you want to learn more about async, await, and Future, visit codelab for asynchronous programming.

How the Isolate works

Modern devices typically use multi-core cpus. Developers sometimes use threads of shared content to run code concurrently to make their programs perform better on devices. However, sharing state can create race conditions that can cause errors, and can also add complexity to your code.

Dart code does not run on multiple threads; instead, it runs within the ISOLATE. Each ISOLATE had its own heap memory to ensure that each isolate could not access each other. Since this implementation doesn’t share memory, you don’t need to worry about mutex and other locks.

With ISOLATE, your Dart code can perform multiple independent tasks at the same time and use available processor cores. The Isolate is similar to threads and processes, but each Isolate has separate memory and separate threads that run event loops.

The main isolate

Under normal circumstances, you don’t need to care about the ISOLATE at all. Typically a Dart application executes all code under the main ISOLATE, as shown below:

Even an application with only one ISOLATE can run smoothly as long as it uses async-await to handle asynchronous operations. An application with good performance will enter the event loop as soon as possible after a quick start. This allows applications to quickly respond to corresponding events through asynchronous operations.

The life cycle of the Isolate

As shown in the figure below, each ISOLATE starts by running the Dart code, such as the main() function. Executing Dart code might register event listeners, such as handling user actions or file reads and writes. When the Dart code executed by the ISOLATE is over, it will continue to be held if it needs to process events that have been monitored. After all events are processed, the ISOLATE exits.

The event processing

In client applications, the event queue of the main ISOLATE may contain redraw requests, click notifications, or other interface events. For example, the following figure shows an event queue containing four events that are processed on a first-in, first-out basis.

As shown in the figure below, processing in the event queue begins after the main() method has finished executing, processing the first redrawn event. The master isolate handles the click event, and then handles another redraw event.

If a synchronous operation takes a long time to process, the application appears to be unresponsive. In the figure below, the code that handles the click event is time-consuming, resulting in subsequent events not being processed in a timely manner. At this point, the application may freeze and all animations will not play smoothly.

In a client application, a synchronization operation that takes too long will often result in a stuttering animation. Worst of all, the interface can become completely unresponsive.

Background running object

If your application is running slow due to time-consuming calculations, such as parsing large JSON files, you can consider moving time-consuming calculations to isolatesthat work independently, which are often called background running objects. The figure below shows a common scenario where you can generate an ISOLATE that performs computationally time-consuming tasks and exits when it’s done. The ISOLATE work object returns the result when it exits.

Each ISOLATE can deliver an object via message communication, and all the contents of this object need to be transmissible. Not all objects meet the delivery criteria, and if they do not, the message will fail to be sent. For example, if you want to send a List, you need to make sure that all elements in the List are passable. Suppose there is a Socket in the list, and since it cannot be passed, you cannot send the whole list.

You can consult the documentation for the send() method to determine which types can be passed.

Isolate Work objects perform I/O operations, set timers, and other activities. It will hold its own memory space, isolated from the main ISOLATE. This ISOLATE blocks without affecting any other isolate.

Code sample

This section focuses on some examples of implementing Isolate using the Isolate API.

Tips for Flutter development

If you are developing with Flutter on non-Web platforms, instead of using the Isolate API directly, consider using the compute() method provided by Flutter, The compute() method encapsulates a function call into the ISOLATE working object in a simple way.

Implement a simple ISOLATE working object

This section shows an implementation of a main ISOLATE and the work objects it generates. The Isolate work object executes a function, terminates the object upon completion, and sends the result of the function to the main Isolate. (The compute() method provided by Flutter works in a similar way.)

The following examples use these ISOLate-related apis:

  • Isolate the spawn () and Isolate the exit ()
  • ReceivePort and SendPort

The code for the main ISOLATE is as follows:

void main() async {
  // Read some data.
  final jsonData = await _parseInBackground();

  // Use that data
  print('number of JSON keys = ${jsonData.length}');
}

// Spawns an isolate and waits for the first message
Future<Map<String.dynamic>> _parseInBackground() async {
  final p = ReceivePort();
  await Isolate.spawn(_readAndParseJson, p.sendPort);
  return await p.first;
}
Copy the code

The _parseInBackground() method contains the code to generate the backstage ISOLATE working object and returns the result:

  1. Before generating the ISOLATE, the code creates oneReceivePortTo allow the ISOLATE work objects to send messages to the main ISOLATE.
  2. Next is the callIsolate.spawn()Generates and starts an ISOLATE working object that runs in the background. The first argument to this method is a function reference executed by the ISOLATE working object:_readAndParseJson. The second parameter is used by the ISOLATE to communicate messages with the main ISOLATESendPort. The code here does notcreateThe newSendPort“, but directly usedReceivePortsendPortProperties.
  3. Once the Isolate has been initialized, the main Isolate waits for its results. Due to theReceivePortTo achieve theStreamYou can use it very easilyfirstProperty to get a single message returned by the ISOLATE work object.

After initialization, the ISOLATE executes the following code:

Future _readAndParseJson(SendPort p) async {
  final fileData = await File(filename).readAsString();
  final jsonData = jsonDecode(fileData);
  Isolate.exit(p, jsonData);
}
Copy the code

After the last line of code, the ISOLATE exits and sends jsonData via the incoming SendPort. When messages are transmitted between the ISOLATE, data copying usually takes place. The time required varies with the size of the data, and the complexity is O(n). However, when you use isolate.exit () to send data, messages held in the ISOLate.exit () are not copied, but directly transferred to the receiving isolate.exit (). Such a transfer is fast and takes only O(1) time.

Isolate.exit()Introduced in Dart 2.15

In previous versions of Dart, only explicit messaging through isolate.send () was supported,

This is illustrated in an example in the next section.

The following figure illustrates the communication flow between the main ISOLATE and the ISOLATE working object:

Send multiple messages between isolate

If you want to establish more communication between the ISOLATE, then you need to use the send() method of SendPort. The following figure shows a common scenario where the main ISOLATE sends request messages to the ISOLATE work objects, and then they continue to communicate with each other many times for requests and replies.

The following example of ISOLATE contains the use of sending multiple messages:

  • Send_and_receive. dart shows how to send messages from the main ISOLATE to the generated isolate. This is similar to the previous example.
  • Long_running_isolate. dart shows how to generate a long-running ISOLATE that sends and receives messages multiple times.

Performance and ISOLATE group

When one ISOLATE calls isolate.spawn (), both isolates have the same execution code and are grouped into the same isolate group. The Isolate group brings performance optimizations, such as the new Isolate running code held by the Isolate group, called shared code. Meanwhile, isolate.exit () is valid only when the corresponding ISOLates belong to the same group.

In some cases you may need to use isolate.spawnuri (), which generates a new Isolate with the executed URI and contains a copy of the code. However, spawnUri() will be much slower than spawn() and the newly generated isolate will reside in the new isolate group. Also, when the ISOLATE is in different groups, messaging between them becomes slower.

Be careful in Flutter development

Flutter does not support isolate.spawnuri ().

The article information

  • Post: Concurrency in Dart
  • Translation: @ AlexV525
  • Proofread: @vadaski, @nayuta403
  • English: dart.cn/guides/lang…