Skip to main content

Isolates

Information on writing isolates in Dart.

This page discusses some examples that use the Isolate API to implement isolates.

You should use isolates whenever your application is handling computations that are large enough to temporarily block other computations. The most common example is in Flutter applications, when you need to perform large computations that might otherwise cause the UI to become unresponsive.

There aren't any rules about when you must use isolates, but here are some more situations where they can be useful:

  • Parsing and decoding exceptionally large JSON blobs.
  • Processing and compressing photos, audio and video.
  • Converting audio and video files.
  • Performing complex searching and filtering on large lists or within file systems.
  • Performing I/O, such as communicating with a database.
  • Handling a large volume of network requests.

Implementing a simple worker isolate

#

These examples implement a main isolate that spawns a simple worker isolate. Isolate.run() simplifies the steps behind setting up and managing worker isolates:

  1. Spawns (starts and creates) an isolate.
  2. Runs a function on the spawned isolate.
  3. Captures the result.
  4. Returns the result to the main isolate.
  5. Terminates the isolate once work is complete.
  6. Checks, captures, and throws exceptions and errors back to the main isolate.

Running an existing method in a new isolate

#
  1. Call run() to spawn a new isolate (a background worker), directly in the main isolate while main() waits for the result:
dart
const String filename = 'with_keys.json';

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

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}
  1. Pass the worker isolate the function you want it to execute as its first argument. In this example, it's the existing function _readAndParseJson():
dart
Future<Map<String, dynamic>> _readAndParseJson() async {
  final fileData = await File(filename).readAsString();
  final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
  return jsonData;
}
  1. Isolate.run() takes the result _readAndParseJson() returns and sends the value back to the main isolate, shutting down the worker isolate.

  2. The worker isolate transfers the memory holding the result to the main isolate. It does not copy the data. The worker isolate performs a verification pass to ensure the objects are allowed to be transferred.

_readAndParseJson() is an existing, asynchronous function that could just as easily run directly in the main isolate. Using Isolate.run() to run it instead enables concurrency. The worker isolate completely abstracts the computations of _readAndParseJson(). It can complete without blocking the main isolate.

The result of Isolate.run() is always a Future, because code in the main isolate continues to run. Whether the computation the worker isolate executes is synchronous or asynchronous doesn't impact the main isolate, because it's running concurrently either way.

For the complete program, check out the send_and_receive.dart sample.

Sending closures with isolates

#

You can also create a simple worker isolate with run() using a function literal, or closure, directly in the main isolate.

dart
const String filename = 'with_keys.json';

void main() async {
  // Read some data.
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });

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

This example accomplishes the same as the previous. A new isolate spawns, computes something, and sends back the result.

However, now the isolate sends a closure. Closures are less limited than typical named functions, both in how they function and how they're written into the code. In this example, Isolate.run() executes what looks like local code, concurrently. In that sense, you can imagine run() to work like a control flow operator for "run in parallel".

Sending multiple messages between isolates with ports

#

Short-lived isolates are convenient to use, but require performance overhead to spawn new isolates and to copy objects from one isolate to another. If your code relies on repeatedly running the same computation using Isolate.run, you might improve performance by instead creating long-lived isolates that don’t exit immediately.

To do this, you can use some of the low-level isolate APIs that Isolate.run abstracts:

This section goes over the steps required to establish 2-way communication between a newly spawned isolate and the main isolate. The first example, Basic ports, introduces the process at a high-level. The second example, Robust ports, gradually adds more practical, real-world functionality to the first.

ReceivePort and SendPort

#

Setting up long-lived communication between isolates requires two classes (in addition to Isolate): ReceivePort and SendPort. These ports are the only way isolates can communicate with each other.

A ReceivePort is an object that handles messages that are sent from other isolates. Those messages are sent via a SendPort.

Ports behave similarly to Stream objects (in fact, receive ports implement Stream!) You can think of a SendPort and ReceivePort like Stream's StreamController and listeners, respectively. A SendPort is like a StreamController because you "add" messages to them with the SendPort.send() method, and those messages are handled by a listener, in this case the ReceivePort. The ReceivePort then handles the messages it receives by passing them as arguments to a callback that you provide.

Setting up ports

#

A newly spawned isolate only has the information it receives through the Isolate.spawn call. If you need the main isolate to continue to communicate with a spawned isolate past its initial creation, you must set up a communication channel where the spawned isolate can send messages to the main isolate. Isolates can only communicate via message passing. They can’t “see” inside each others’ memory, which is where the name “isolate” comes from.

To set up this 2-way communication, first create a ReceivePort in the main isolate, then pass its SendPort as an argument to the new isolate when spawning it with Isolate.spawn. The new isolate then creates its own ReceivePort, and sends its SendPort back on the SendPort it was passed by the main isolate. The main isolate receives this SendPort, and now both sides have an open channel to send and receive messages.

A figure showing events being fed, one by one, into the event loop

  1. Create a ReceivePort in the main isolate. The SendPort is created automatically as a property on the ReceivePort.
  2. Spawn the worker isolate with Isolate.spawn()
  3. Pass a reference to ReceivePort.sendPort as the first message to the worker isolate.
  4. Create another new ReceivePort in the worker isolate.
  5. Pass a reference to the worker isolate's ReceivePort.sendPort as the first message back to the main isolate.

Along with creating the ports and setting up communication, you’ll also need to tell the ports what to do when they receive messages. This is done using the listen method on each respective ReceivePort.

A figure showing events being fed, one by one, into the event loop

  1. Send a message via the main isolate’s reference to the worker isolate's SendPort.
  2. Receive and handle the message via a listener on the worker isolate's ReceivePort. This is where the computation you want to move off the main isolate is executed.
  3. Send a return message via the worker isolate's reference to the main isolate's SendPort.
  4. Receive the message via a listener on the main isolate's ReceivePort.

Basic ports example

#

This example demonstrates how you can set up a long-lived worker isolate with 2-way communication between it and the main isolate. The code uses the example of sending JSON text to a new isolate, where the JSON will be parsed and decoded, before being sent back to the main isolate.

Step 1: Define the worker class

#

First, create a class for your background worker isolate. This class contains all the functionality you need to:

  • Spawn an isolate.
  • Send messages to that isolate.
  • Have the isolate decode some JSON.
  • Send the decoded JSON back to the main isolate.

The class exposes two public methods: one that spawns the worker isolate, and one that handles sending messages to that worker isolate.

The remaining sections in this example will show you how to fill in the class methods, one-by-one.

dart
class Worker {
  Future<void> spawn() async {
    // TODO: Add functionality to spawn a worker isolate.
  }

  void _handleResponsesFromIsolate(dynamic message) {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _startRemoteIsolate(SendPort port) {
    // TODO: Define code that should be executed on the worker isolate.
  }

  Future<void> parseJson(String message) async {
    // TODO: Define a public method that can
    // be used to send messages to the worker isolate.
  }
}