# Isolates

Course: [Dart](https://www.devtutorials.ai/learn/dart) · Section: Asynchronous Programming
Source: https://www.devtutorials.ai/learn/dart/async/isolates

> 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.

```dart runnable title="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 |
|---|---|---|
| Memory | shared by all threads | separate for each isolate |
| Talking | change shared variables | send messages |
| Common bugs | two threads change one value at once | almost none of these |
| Cost | cheap to share data | data 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.

```dart runnable title="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".

**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

```dart runnable title="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:

```dart runnable title="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, `SendPort`s, 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

| Action | Code | When |
|---|---|---|
| Stop listening | `port.close()` | you need no more messages |
| Stop a worker | `isolate.kill()` | the job is no longer needed |
| Hear about errors | `Isolate.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]`.

```dart runnable title="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

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Changing a list inside a worker | The main list changes too | The worker has a copy | Main never sees the change. |
| Forgetting `inbox.close()` | Ports close by themselves | Close every port | The program never ends. |
| Using isolates for tiny jobs | More isolates are always faster | Starting one costs time | The app gets slower. |
| Sending a `ReceivePort` | Anything can be sent | Send its `sendPort` instead | An error. |
| Expecting `Isolate.spawn` on the web | It works everywhere | Use `compute` in Flutter, or web workers | It 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.
