Isolates

By Syncfusion®, Inc. · Last updated

How isolates run heavy work in the background with their own memory: Isolate.run for one job, Isolate.spawn with ReceivePort and SendPort for messages, and closing, killing and error listeners.

What Is an Isolate?

Isolate = a separate worker that runs Dart code at the same time as your main program, with its own memory.

  • Your main() already runs in one isolate: the main isolate.
  • You can start more isolates for heavy work, like adding up a million numbers or reading a huge file.
  • Isolates do not share memory. They talk only by sending messages.

Simple lesson: an isolate is a helper who works in a separate room, and you pass notes under the door.

A Real-World Example

Report card day at school.

  • The class teacher must also keep teaching. She cannot stop for three hours to add up marks.
  • So she gives the mark sheets to a helper in the staff room.
  • The helper works with their own copy of the sheets, not hers.
  • When done, the helper sends back a note: "Class average: 78.4".
  • The teacher's class never stopped. That is what isolates do for your app.

On your computer, isolates run truly at the same time, on different processor cores.

The course runner simulates isolates on one thread, so you can practise the code. Results and message order are the same, but there is no real speed-up. On the web, Dart cannot use Isolate.spawn at all.

What You Will Learn

In this lesson, you will learn:

  1. What isolates are, and why they have separate memory.
  2. How isolates differ from threads in other languages.
  3. How to run one job with Isolate.run().
  4. How to send messages with Isolate.spawn(), ReceivePort and SendPort.
  5. How to close ports, kill isolates, and listen for errors.

Isolates and Memory

Memory Isolation

Memory isolation = each isolate has its own variables. One isolate cannot read or change another's.

  • When you send a value to another isolate, it gets a copy.
  • Changing the copy does not change the original.

The worker changes only its copy

import 'dart:isolate';

void addMark(List<Object> message) {
  var marks = message[0] as List<int>;
  var reply = message[1] as SendPort;
  marks.add(100);
  reply.send(marks.length);
}

Future<void> main() async {
  var marks = [70, 80, 90];
  var inbox = ReceivePort();
  await Isolate.spawn(addMark, [marks, inbox.sendPort]);
  var workerCount = await inbox.first;
  print('Worker saw $workerCount marks');
  print('Main still has ${marks.length} marks');
  inbox.close();
}

You will learn Isolate.spawn, ReceivePort and SendPort below. For now, see the result: the worker added a mark to its list only.

Isolates vs Threads

Threads (Java, C++)Dart isolates
Memoryshared by all threadsseparate for each isolate
Talkingchange shared variablessend messages
Common bugstwo threads change one value at oncealmost none of these
Costcheap to share datadata is copied in messages

Because nothing is shared, two isolates can never fight over one variable. That makes isolates safe.

Simple lesson: isolates are safe because each one has its own memory, and they only exchange copies.

Isolate.run()

Isolate.run(function) = the simplest way. It starts an isolate, runs one function, returns its result as a Future, and closes the isolate.

Adding up big numbers in the background

import 'dart:isolate';

int sumUpTo(int n) {
  var total = 0;
  for (var i = 1; i <= n; i++) {
    total += i;
  }
  return total;
}

Future<void> main() async {
  print('Starting the heavy job...');
  var total = await Isolate.run(() => sumUpTo(1000000));
  print('Total: $total');
}
  • While the worker adds, the main isolate stays free. In an app, the screen keeps moving.
  • Use Isolate.run for most jobs: "do this one thing, give me the answer".

What Isolate.run does

Simple lesson: Isolate.run sends one job to a helper and gives you the answer as a Future.

Isolate.spawn with Ports

For long-running helpers that send many messages, use Isolate.spawn with ports.

  • ReceivePort = a mailbox. It is also a Stream of the messages that arrive.
  • SendPort = the address of a mailbox. Anyone with it can send messages there.
  • Isolate.spawn(entry, message) = start an isolate running entry(message).

Messages from the Worker

A worker that reports progress

import 'dart:isolate';

void checkPapers(SendPort mailbox) {
  for (var paper = 1; paper <= 3; paper++) {
    mailbox.send('Paper $paper checked');
  }
  mailbox.send('done');
}

Future<void> main() async {
  var inbox = ReceivePort();
  await Isolate.spawn(checkPapers, inbox.sendPort);

  await for (var message in inbox) {
    if (message == 'done') {
      inbox.close();
    } else {
      print(message);
    }
  }
  print('All papers checked');
}
  • main gives the worker the address of its mailbox: inbox.sendPort.
  • The worker sends notes with mailbox.send(...).
  • main reads them with await for, because a ReceivePort is a Stream.
  • close() ends the Stream, so the loop can finish.

Talking Both Ways

To send messages to the worker too, the worker makes its own mailbox and sends its address back first:

A worker that answers questions

import 'dart:isolate';

void squarer(SendPort replyTo) async {
  var myInbox = ReceivePort();
  replyTo.send(myInbox.sendPort);
  await for (var n in myInbox) {
    if (n == 0) {
      myInbox.close();
    } else {
      replyTo.send(n * n);
    }
  }
}

Future<void> main() async {
  var inbox = ReceivePort();
  await Isolate.spawn(squarer, inbox.sendPort);

  SendPort? worker;
  var asked = 0;
  await for (var msg in inbox) {
    if (msg is SendPort) {
      worker = msg;
      worker.send(4);
      asked++;
    } else {
      print('Answer: $msg');
      if (asked < 2) {
        worker!.send(9);
        asked++;
      } else {
        worker!.send(0);
        inbox.close();
      }
    }
  }
}

Only some values can be sent: numbers, strings, booleans, null, lists, maps, SendPorts, and objects made of them.

Things tied to the current isolate, like an open ReceivePort or a running Stream, cannot be sent.

Simple lesson: a ReceivePort is your mailbox, and you give its SendPort to anyone who should write to you.

Closing, Killing and Errors

ActionCodeWhen
Stop listeningport.close()you need no more messages
Stop a workerisolate.kill()the job is no longer needed
Hear about errorsIsolate.spawn(f, msg, onError: port.sendPort)a worker might crash

When a spawned worker crashes, its error arrives at the onError port as a list: [error text, stack trace text].

Hearing about a worker's crash

import 'dart:isolate';

void badWorker(int n) {
  throw StateError('Paper $n is missing');
}

Future<void> main() async {
  var errors = ReceivePort();
  await Isolate.spawn(badWorker, 7, onError: errors.sendPort);
  var report = await errors.first;
  print('Worker failed: ${report[0]}');
  errors.close();
}
  • An open ReceivePort keeps the program alive. Close every port you open.
  • With Isolate.run, errors are simpler: the worker's exception is thrown at your await, so use try and catch.

Easy way to remember

  • Isolate = a helper in another room, with its own copy of everything.
  • Isolate.run = "do this one job and tell me the answer".
  • ReceivePort = my mailbox. SendPort = my address.
  • close / kill / onError = tidy up, send home, hear about problems.

Simple lesson: close the ports you open, kill workers you no longer need, and listen for their errors.

TL;DR

  1. Isolate = separate worker, own memory, talks by messages (copies).
  2. Unlike threads, isolates share nothing, so they cannot fight over a variable.
  3. Isolate.run(() => work()) runs one job and gives a Future of the result.
  4. Isolate.spawn(entry, sendPort) + ReceivePort for many messages. A ReceivePort is a Stream.
  5. close() ports, kill() isolates, onError: to hear crashes. Not available on the web.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Changing a list inside a workerThe main list changes tooThe worker has a copyMain never sees the change.
Forgetting inbox.close()Ports close by themselvesClose every portThe program never ends.
Using isolates for tiny jobsMore isolates are always fasterStarting one costs timeThe app gets slower.
Sending a ReceivePortAnything can be sentSend its sendPort insteadAn error.
Expecting Isolate.spawn on the webIt works everywhereUse compute in Flutter, or web workersIt fails in the browser.

Check Yourself

Exercises

What to Remember

  • An isolate is a separate worker with its own memory, and isolates talk only by messages.
  • Messages are copies, so isolates can never change each other's variables.
  • Isolate.run is the easy way to run one heavy job in the background.
  • Isolate.spawn with a ReceivePort and its SendPort handles many messages.
  • Close every port, kill unneeded isolates, and use onError: to hear about crashes.

Next: File Handling. You will read and write files on your computer.