skip to content

In Dart, why is Isolate.spawn much cheaper than Isolate.spawnUri, and how does Isolate.exit hand a large result back without copying it?

level: seniorimportance: should knowfreq 24%

answer

  1. same program, same group
  2. shared code, shared managed heap
  3. spawnUri loads separate code
  4. exit transfers the final message
  5. no finally, no further events

basics

~20 s

Isolate.spawn puts the new isolate in the spawner's isolate group, sharing code and a managed heap, so it starts fast. spawnUri loads a separate program outside the group. Within a group, Isolate.exit transfers its final message's objects to the receiver instead of copying them.

solid answer

~50 s

Since Dart 2.15, isolates created with `Isolate.spawn` join the spawner's **isolate group**: they run the same compiled code and live on a shared managed heap, while each still has its own globals and cannot see others' objects. That made spawning much faster and lighter, and made messages within the group cheaper. `Isolate.spawnUri` instead loads a separate Dart program from a URI; the new isolate is outside the group, starts more slowly, and exchanges messages more slowly. `Isolate.exit(port, message)` terminates the current isolate immediately and, when the port belongs to an isolate in the same group, hands the message's object graph to the receiver **without copying** — which is how `Isolate.run` returns results. The trade-off: `exit` stops the isolate on the spot, skipping pending `finally` blocks and queued events, and it throws if the port belongs to a `spawnUri` isolate.

code

dart · 19 lines
dart
import 'dart:io';
import 'dart:isolate';

int checksum(List<int> bytes) =>
    bytes.fold(0, (hash, b) => (hash * 31 + b) & 0xffffffff);

void hashWorker((SendPort, List<String>) message) {
  final (reply, paths) = message;
  final hashes = <String, int>{
    for (final p in paths) p: checksum(File(p).readAsBytesSync()),
  };
  Isolate.exit(reply, hashes); // transferred, not copied; nothing runs after
}

Future<Map<String, int>> hashInWorker(List<String> paths) async {
  final port = ReceivePort();
  await Isolate.spawn(hashWorker, (port.sendPort, paths), debugName: 'hasher');
  return await port.first as Map<String, int>;
}

go deeper

for a junior

Know that Isolate.spawn reuses the running program's code, while spawnUri starts a separate program.

for a middle

Explain isolate groups: shared code and heap, separate globals, cheaper spawning and messaging since Dart 2.15.

for a senior

Use Isolate.exit to return large results without a copy, and account for its abrupt termination: no finally blocks and no queued events.

for a principal

Weigh in-group workers against separately loaded programs when designing plug-in or sandbox boundaries, trading speed for code independence.

## Isolate groups An **isolate group** is a set of isolates created from the same program. When one isolate calls `Isolate.spawn`, the new isolate joins its group. Since Dart 2.15, isolates in a group: - **share compiled code** — the new isolate starts running the group's code immediately instead of loading or compiling it; - **share a managed heap** that the VM garbage-collects collaboratively; - still have **separate global state** — each isolate initialises its own globals and statics, and no isolate can reach another's objects. The 2.15 release notes put numbers on it: much faster startup, much lower base memory per isolate, and faster communication between isolates. The `Isolate.spawn` documentation now cites a base overhead in the order of 30 kb. ## spawnUri: a different program `Isolate.spawnUri(uri, args, message, {...})` starts an isolate from a Dart source file or snapshot at `uri`, calling its `main(args, message)`. That isolate: 1. loads its own code rather than sharing the spawner's; 2. is **not** in the spawner's isolate group; 3. exchanges messages more slowly and under stricter rules about what can be sent; 4. cannot receive a transfer through `Isolate.exit`. It suits plug-in style architectures or running a separately compiled program, and dart.dev calls it much slower than `spawn`. For offloading work inside one app, `spawn` or `run` is the answer. ## Isolate.exit: a final message by transfer `Isolate.exit([SendPort? finalMessagePort, Object? message])` does three things: 1. checks that `message` can be sent (it throws if not); 2. sends it through `finalMessagePort`; 3. terminates the current isolate **immediately** — its return type is `Never`. When the port is an ordinary `ReceivePort` or `RawReceivePort` port of an isolate in the same group, the VM can **reassign** the message's objects to the receiving isolate instead of copying them, and the receiver usually gets it in constant time. A normal `SendPort.send` between live isolates has to copy the graph, since both sides keep running and must not share mutable objects. For the backup CLI, a worker that hashes a batch and builds a map of 50,000 entries can end with `Isolate.exit(reply, hashes)`: the main isolate receives the map without paying for a second copy. | | `SendPort.send` | `Isolate.exit(port, message)` | |---|---|---| | Sender keeps running | Yes | No — it terminates | | Large graph within a group | Copied | Transferred without copying | | Port in another group (`spawnUri`) | Allowed, with stricter rules | Throws | | Pending `finally` blocks and events | Unaffected | Skipped | ## The costs of exiting abruptly Because `exit` never returns control to the event loop: - pending `finally` blocks do not run, so close files and flush buffers **before** calling it; - scheduled timers, microtasks and queued messages are dropped; - exit listeners registered with `addOnExitListener` are still notified. ## How this shows up in Isolate.run `Isolate.run` is built on exactly these pieces: it spawns into the same group, runs your computation, and returns the result with `Isolate.exit`. That is why a large result from `Isolate.run` comes back cheaply, and why the recommended pattern for a one-shot worker is `run` rather than hand-rolled `spawn` plus `send`.

  • Does sharing a heap within an isolate group mean isolates can share mutable objects?
    No. The shared heap is a VM implementation detail for memory management and fast transfer. Each isolate still has its own globals and can only obtain another isolate's data through messages, so Dart code sees no shared mutable state.
  • What happens to a try/finally that is still open when a worker calls Isolate.exit?
    The `finally` block does not run. `Isolate.exit` terminates the isolate immediately without returning to its callers or its event loop, so any cleanup — closing files, flushing logs — must happen before the call.

saying these in an interview costs you the question

  • Isolate.spawnUri isolates share the spawner's code and group.
  • Isolate.exit copies the message just like SendPort.send.
  • Code in finally blocks still runs after Isolate.exit.
  • Isolates in one group can read each other's globals.
  • Isolate.exit works with any port, including a spawnUri isolate's.