skip to content

Spawning Workers

Isolate.run offloads one computation and returns its result, while Isolate.spawn starts a long-lived worker from an entry point in the same isolate group. Interviewers ask when async is not enough.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Dart, why doesn't marking a CPU-heavy function async keep the program responsive, and what does running it in another isolate change?

level: juniorimportance: must knowfreq 70%

answer

  1. async waits, it does not compute elsewhere
  2. body runs synchronously until await
  3. one event loop per isolate
  4. separate heap, no shared memory
  5. parallel on another core

basics

~20 s

An async function still runs on the calling isolate's single thread; its synchronous work blocks that isolate's event loop until the next await. Another isolate has its own heap and event loop and can run the work in parallel on another core.

solid answer

~50 s

`async`/`await` lets one isolate interleave work while it *waits* — for a file, a socket, a timer. It does not move computation anywhere: an `async` function runs synchronously on the caller's isolate until its first `await`, so a loop hashing thousands of files blocks that isolate's event loop the whole time — no timers fire, no I/O callbacks run, and in Flutter no frames are produced. An **isolate** is a separate Dart execution context with its own heap, globals and event loop; the VM can run it on another core in parallel. Isolates share no mutable memory — they communicate by messages — so there are no locks or data races, but data must be sent across. Unlike threads in Java or Kotlin, which share one heap, an isolate cannot read the main isolate's objects directly.

code

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

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

// Blocks the calling isolate for the whole loop despite `async`.
Future<Map<String, int>> hashAllBlocking(List<String> paths) async =>
    {for (final p in paths) p: checksum(File(p).readAsBytesSync())};

// Runs the same loop in a worker isolate; the caller stays free.
Future<Map<String, int>> hashAllInWorker(List<String> paths) =>
    Isolate.run(() =>
        {for (final p in paths) p: checksum(File(p).readAsBytesSync())});

go deeper

for a junior

Say clearly that async only helps while waiting, and that heavy computation needs another isolate to avoid blocking.

for a middle

Explain that an async body runs synchronously until its first pending await, and that isolates have separate heaps and communicate only by messages.

for a senior

Decide between async and isolates from whether work is CPU-bound or I/O-bound, and account for spawn and transfer costs and the lack of isolates on the web.

for a principal

Frame isolate use as an architectural boundary: which components own CPU-heavy work, what data crosses the boundary, and how that shapes APIs.

## What async actually does Every piece of Dart code runs inside an **isolate**: a thread of control with its own memory and a single **event loop** that processes one event at a time. `async` and `await` are tools for *waiting* without blocking that loop: - An `async` function runs **synchronously** from its first line until it reaches an `await` on a future that is not yet complete. - At that point it suspends, returns a `Future` to its caller, and the event loop is free to handle other events. - When the awaited future completes, the rest of the function is scheduled as another event on the **same** isolate. Nothing in that sequence runs code on another core. If the function body is a tight loop that computes — hashing, parsing, compressing, image processing — there is no `await` to yield at, and the whole computation occupies the isolate until it finishes. ## The symptom Consider a backup command-line tool that computes a checksum for every file before upload: ```dart Future<Map<String, int>> hashAll(List<String> paths) async { return {for (final p in paths) p: checksum(File(p).readAsBytesSync())}; } ``` The `async` keyword changes only the return type. For ten thousand files the loop runs start to finish in one go: the progress timer never ticks, a Ctrl-C handler registered with `ProcessSignal` waits, and in a Flutter app the same code would freeze animations because the UI isolate cannot build frames. ## What an isolate changes | Property | Same isolate with async | Worker isolate | |---|---|---| | Where the CPU work runs | The caller's thread, blocking its event loop | A separate isolate the VM can run on another core | | Memory | Shared with all code in the isolate | Its own heap and its own copy of every global | | Communication | Direct object access | Messages: arguments in, result out | | Data races | Impossible (one thread) | Impossible (no shared mutable state) | | Cost | Nothing extra | Spawn time, some memory, and moving data across | In practice: 1. **Parallelism.** With `Isolate.run` or `Isolate.spawn`, the checksum loop runs in another isolate while the main isolate keeps handling its events. On a multi-core machine several worker isolates hash files at the same time. 2. **Isolation.** A worker cannot touch the main isolate's objects. A global counter incremented in the worker stays unchanged in the main isolate, because each isolate has its own copy. 3. **Messaging.** Inputs travel with the spawn call and results come back as messages. Within one program this is cheap, but not free, and some objects — open files and sockets, for example — cannot be sent at all. ## Isolates versus threads Threads in languages such as Java or Kotlin share one heap: any thread can read and write any object, so correctness depends on locks and memory-visibility rules. Dart isolates trade that for **isolation**: there is no shared mutable state, so no locks and no data races, but also no reaching into another isolate's data. Isolates can still have *race conditions* at the level of message ordering; they just cannot corrupt each other's memory. ## When to reach for one - The work is **CPU-bound** and long enough to delay other events noticeably. - The inputs and outputs are data that can be sent between isolates. - You are on a **native** platform: the Dart web platform does not implement isolates. For I/O-bound work — many network calls, many file reads — plain `async` with `Future.wait` already overlaps the waiting, and an isolate adds cost without benefit.

  • Would changing readAsBytesSync to await readAsBytes make the hashing loop responsive?
    Only partly. Each `await` lets the event loop run other events while a file is read, so timers and I/O callbacks get turns between files. The checksum of each file is still computed synchronously on the same isolate, so a large file still blocks for its whole hash, and no work runs in parallel.
  • If a worker isolate increments a top-level counter, what does the main isolate see?
    Nothing. Each isolate has its own copy of every global and static field, initialised independently. The worker's increment changes only its own copy; to report a count, the worker must send it back as a message or return it.

A single cook who can put several pots on the stove and wait on all of them, but who still has to do every bit of chopping personally; an isolate is a second kitchen with its own cook, receiving orders and sending back finished plates through a hatch.

saying these in an interview costs you the question

  • Marking a function async runs it on a background thread.
  • An await-free async function yields to the event loop between iterations.
  • Isolates share the heap, so a worker can update the caller's objects.
  • Isolates need locks to protect shared data.
  • An isolate always speeds up I/O-bound work like many HTTP calls.
open as a page

In Dart, when would you use Isolate.run and when Isolate.spawn, for example to hash thousands of files in a backup CLI?

level: middleimportance: must knowfreq 58%

basics

~20 s

Isolate.run starts an isolate, runs one function, returns its result as a Future, rethrows its error, and exits. Isolate.spawn starts a long-lived worker from an entry point and a message, and you manage ports, results, errors and shutdown yourself.

open as a page

In Dart, what happens when code that calls Isolate.run or Isolate.spawn is compiled for the web, and what are the alternatives?

level: middleimportance: should knowfreq 28%

basics

~20 s

The Dart web platform does not implement isolates: under the JavaScript compiler, calls such as Isolate.spawn throw UnsupportedError. Web code must run the work on the main thread, split into chunks, or move it into a separately compiled web worker.

open as a page

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%

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.

open as a page

A Dart backup CLI hangs forever after a spawned hashing isolate hits an unreadable file — why, and how do onError, onExit and kill prevent it?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The worker threw, errors are fatal by default, so it died without sending a result, and the spawner's await on its port waits forever. Pass onError and onExit ports to Isolate.spawn so a crash or silent exit is reported, and kill workers you no longer need.

open as a page