In Dart, how do two isolates exchange data through SendPort and ReceivePort, and what happens to a mutable object you send?
answer
- no shared mutable state between isolates
- ReceivePort exposes a sendPort getter
- send() returns without waiting
- mutable graphs copied, immutable ones shared
- receiver edits its own copy
basics
~20 sIsolates share no mutable state, so they talk by messages: a ReceivePort receives them, and its sendPort getter gives a SendPort that any isolate can call send() on. Mutable objects arrive as copies; immutable ones such as strings are shared.
solid answer
~50 sEach Dart isolate has its own memory and its own event loop, so the only way to pass data is a message. You create a `ReceivePort` in the isolate that wants to receive, and hand its `sendPort` to the other isolate, usually as the spawn argument. Calling `SendPort.send(message)` returns immediately; the message is delivered to the receiving port's listener as an ordinary event when that isolate's event loop gets to it. The message is not shared by reference: the mutable part of its object graph is copied during `send()`, while objects the VM knows are immutable, such as strings, are shared. So if the worker adds to a list it received, the sender's list is unchanged. One `ReceivePort` can have many `SendPort`s, and a `SendPort` can itself be sent, which is how two-way channels are built.
code
dart · 16 linesimport 'dart:isolate';
Future<void> main() async {
final replies = ReceivePort();
final sizes = [640, 1280];
await Isolate.spawn(_worker, (replies.sendPort, sizes));
final fromWorker = await replies.first as List<int>;
print(sizes); // [640, 1280]
print(fromWorker); // [640, 1280, 1920]
}
void _worker((SendPort, List<int>) message) {
final (replyTo, sizes) = message;
sizes.add(1920); // changes the worker's copy only
replyTo.send(sizes);
}go deeper
Recall the pair: a ReceivePort receives, its sendPort getter gives the SendPort others call send() on. Say clearly that a sent mutable object arrives as a copy.
Explain that the copy happens inside send() on the sender, that delivery is an event on the receiver's loop, and that immutable objects like strings are shared rather than copied.
Tie the copy cost to real symptoms: a huge message sent from the UI isolate costs frame time even though the work is offloaded, so you shape messages to be small or move bytes differently.
Frame ports as the price of having no shared mutable state: no locks or races on Dart objects, but every boundary crossing is a copy you design and measure.
## Why isolates need ports at all A Dart **isolate** is an independent worker with its own memory and its own **event loop**. Dart code in one isolate cannot read or write a variable that lives in another: a top-level variable mutated in a spawned isolate stays untouched in the main isolate, because each isolate has its own copy of it. This is deliberate. With no shared mutable state there are no data races on Dart objects, no locks and no memory-visibility bugs between isolates. The price is that isolates cannot simply call each other or look at each other's objects. They cooperate by sending **messages**, and `dart:isolate` gives exactly two classes for that job, `ReceivePort` and `SendPort`. ## The two ends of a channel | Class | Lives in | What you do with it | |---|---|---| | `ReceivePort` | the isolate that receives | `listen` to it like a `Stream`, then `close()` it when finished | | `SendPort` | anywhere it has been passed | call `send(message)` to deliver into its `ReceivePort` | - A `ReceivePort` is created with its constructor, `ReceivePort()`. It is a **non-broadcast stream**: it buffers messages until a listener subscribes, and only one listener may subscribe. - Every `ReceivePort` has a **`sendPort` getter**. `SendPort` has no public constructor; you always get one from a receive port. - One `ReceivePort` can have many `SendPort`s pointing at it, and `SendPort`s compare equal when they point at the same receive port, even after being sent to another isolate. - A `SendPort` is itself sendable, so an isolate can pass its own `sendPort` to another isolate. That is how a reply channel, or a full two-way channel, is set up. ## What `send()` actually does 1. You call `sendPort.send(message)` in the sending isolate. 2. The runtime checks the message graph is sendable and copies whatever needs copying. This happens **during the call**, on the sending isolate, and can take time linear in the size of the object graph. 3. `send()` returns. It does **not** wait for the receiver to handle the message and it returns `void`, not a `Future`. 4. When the receiving isolate's event loop is free, the message is delivered to the `ReceivePort`'s listener as a normal data event. Because delivery is an event on the receiver's loop, a busy receiver simply handles the message later; the sender is never blocked by it. ## Copy versus share - **Mutable objects are copied.** Lists, maps, sets, typed-data buffers and instances of your own mutable classes arrive as a new object graph in the receiving isolate, and nothing links the copy back to the original. - **Immutable objects may be shared.** The `SendPort.send` documentation says objects the VM identifies as immutable, for example strings, are shared instead of copied. You cannot observe the difference, because nobody can mutate them. - **Mutation never travels back.** If the worker changes its copy, the sender sees nothing until the worker sends a new message with the result. - **Copying costs time and memory.** Sending a very large structure from the UI isolate of a Flutter app does the copy on that isolate, so it can cost frame time on its own. ## One direction per port A port pair is a **one-way** channel: messages flow from the `SendPort` holders into the single `ReceivePort`. That shapes every design built on it: - **Replies need their own port.** A request that expects an answer carries, or was preceded by, a `SendPort` that belongs to the requester. - **Many writers, one reader.** Several isolates can hold copies of one `SendPort`, and all of their messages end up at the same listener, which handles them one at a time on its event loop. - **Addresses travel, receivers do not.** A `SendPort` can be passed along inside any message, but the `ReceivePort` stays in the isolate that created it. ## A round trip in practice The usual first program spawns a worker with `Isolate.spawn`, passing the main isolate's `sendPort` so the worker can reply. The worker edits the list it received and sends it back; printing both lists shows two independent objects. `await replies.first` takes the first message and then cancels the subscription, which also closes the port, so the program can exit. ## Mistakes interviewers listen for - Saying isolates are threads that share objects. Isolates spawned into one isolate group do use one managed heap and share code internally, but no mutable Dart object is ever reachable from two isolates. - Expecting `send()` to be awaitable or to deliver a reply. Replies need a second port. - Believing every object is copied, strings included, or that nothing is copied because Dart passes references. - Forgetting that a `ReceivePort` accepts only one listener, and that an open port keeps its isolate alive until you close it.
- If send() returns immediately, who pays for copying a large message?The sending isolate. The `SendPort.send` documentation says the send happens immediately and may cost time linear in the object graph it copies. So sending a big list from a Flutter UI isolate spends that copy time on the UI isolate, even though the processing then happens elsewhere.
- How does the worker send an answer back if SendPort.send returns nothing?It needs a port that belongs to the caller. The caller creates a `ReceivePort`, sends or spawns with its `sendPort`, and the worker calls `send` on that. For a conversation in both directions the worker also creates its own `ReceivePort` and sends its `sendPort` back as its first message.
- Can the same SendPort be used by several isolates at once?Yes. A `SendPort` can be sent to as many isolates as you like, and all of their messages arrive at the single `ReceivePort` it came from. Several `SendPort`s can point at one receive port, but each `SendPort` belongs to exactly one `ReceivePort`.
Two isolates are two kitchens with a pass-through hatch. A cook can push a plate through, but the other kitchen gets its own plate; whatever it adds never appears on the plate the first cook still holds.
saying these in an interview costs you the question
- Isolates share objects like threads, so the worker's changes appear in the caller's list
- SendPort.send returns a Future that completes with the reply
- You create a SendPort with its own constructor
- Every sent object is copied, even strings
- send() blocks until the receiving isolate has handled the message