Isolates & True Concurrency
Isolate.run, spawn, ports, message copying, worker pools, TransferableTypedData, DevTools
- 01
Your teammate wraps the parser in compute() and you wrap yours in Isolate.run() — is there any difference, and when is neither enough?
MediumOn native platforms there is no difference at all: Flutter's
computeis implemented asIsolate.run(() => callback(message), debugName: debugLabel).import 'dart:convert'; import 'dart:isolate'; import 'package:flutter/foundation.dart'; // 1. compute — one argument in, one value out, named in the timeline. Future<List<Order>> parseWithCompute(String body) => … - 02
Sending a job into a worker throws "Illegal argument in isolate message: object is unsendable" — what did you put in the message?
MediumSomething in the transitive object graph of the message holds a native resource: the VM walks every field of everything you send, and one open handle anywhere in that graph fails the whole send.
import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'package:flutter/foundation.dart'; // ❌ The closure is written where an open file handle is in scope. … - 03
Handing a worker a 40 MB response body costs 0.1 ms, but handing it the same 40 MB as a byte array costs 4 ms — why?
MediumSendPort.senddeep-copies the transitive object graph, with one exception: values the VM identifies as immutable — strings, numbers, booleans,null— are shared by reference between isolates in the same group.import 'dart:convert'; import 'dart:isolate'; import 'dart:typed_data'; // Measured on a desktop VM with 40 MB payloads — time for send() to return: // String 0.13 ms immutable, shared by reference … - 04
Your isolate spawns fine, but the first job you send it vanishes — what does the ReceivePort handshake have to look like?
MediumA
SendPortis one-way, soIsolate.spawncan only give the child a way to reach you — the child has to create its ownReceivePortand send itsSendPortback before you have anywhere to send work.import 'dart:async'; import 'dart:isolate'; class Worker { Worker._(this._isolate, this._fromWorker, this._toWorker); … - 05
An exception inside a spawned isolate never reaches your try/catch and the screen just spins forever — where did that error go?
MediumIt went to stderr.
import 'dart:isolate'; Future<void> supervise() async { final results = ReceivePort(); final errors = ReceivePort(); final exits = ReceivePort(); … - 06
You call isolate.kill() on a worker stuck in a two-second loop and it finishes the loop anyway — what did kill actually promise?
Mediumkill()defaults topriority: Isolate.beforeNextEvent, which means shut down when control next returns to the event loop — and a synchronous loop is one event, so the worker finishes it first.import 'dart:isolate'; void spinner(SendPort toParent) { toParent.send('started'); final sw = Stopwatch()..start(); while (sw.elapsedMilliseconds < 1500) {} // no await: this is ONE event … - 07
Your worker isolate increments a global counter and the UI isolate never sees the change — where did the shared state go?
MediumEvery isolate has its own heap and its own copy of every global and static variable, initialised from scratch — the worker incremented its own counter, not yours.
import 'dart:isolate'; import 'package:flutter/foundation.dart'; int requestCount = 0; // ONE COPY PER ISOLATE late final Map<String, Rule> rules = loadRules(); // rebuilt per isolate … - 08
How big does a JSON payload have to get before moving the decode to an isolate pays for itself, and how do you find that number?
HardThere is no universal threshold — you measure the decode on the slowest device you ship, in a profile build, and compare it against the frame budget: 16.7 ms at 60 Hz, 8.3 ms at 120 Hz.
import 'dart:convert'; import 'dart:developer'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; … - 09
You call
Isolate.runonce per list row and the grid still stutters — when does a long-lived worker beat spawning per task?HardA spawn is cheap but not free, and it throws away everything the isolate learned — once the work per call is the same order as the spawn plus the message copy, a warm worker wins.
// ❌ a spawn per row: the isolate is born, parses once, and dies again Future<Uint8List> thumbFor(Uint8List src) => Isolate.run(() => makeThumb(src)); // ✅ one worker, many requests, replies matched by id class Thumbnailer { … - 10
One 40-megapixel image ties up your thumbnail pool and the whole grid stops — how do you size and schedule that pool?
HardSize the pool from
Platform.numberOfProcessors, then hand each job to whichever worker has gone idle instead of assigning jobs to workers up front — static assignment is what lets one slow job block the three queued behind it.class ThumbPool { ThumbPool(this.size); final int size; final _idle = <Worker>[]; final _queue = <Job>[]; final _cancelled = <int>{}; … - 11
A worker hands back a 40 MB buffer and both isolates spike — what does
TransferableTypedDataactually move, and what survives on the sender?HardIt makes the send constant-time instead of proportional to the payload, but
TransferableTypedData.fromListstill copies the bytes once — the move happens on the wire, not at construction.import 'dart:isolate'; import 'dart:typed_data'; // ❌ every hop copies the whole buffer, on the sender's turn void decodeAndForward(Uint8List raw, SendPort next) { final pixels = decode(raw); // Uint8List, 40 MB … - 12
Isolate.spawnhappily sends yourProductobject but the same send to aspawnUriisolate throws — what is different about the two?HardIsolate.spawnstarts the new isolate inside the caller's isolate group — same program, same compiled code, same class identities — whilespawnUriloads a second, unrelated program, so the two sides share no classes and almost nothing can cross between them.// ── Same group: Isolate.spawn ────────────────────────────── class Product { Product(this.id, this.name); final int id; final String name; } … - 13
compute()on Flutter web returns the right answer and still freezes the tab for two seconds — what is it doing there?HardThe web has no isolates, and Flutter's web
computeis literallyawait null;followed byreturn callback(message);— one microtask yield, then your function, on the browser's main thread.// ── work_io.dart — mobile/desktop: a genuine isolate ─────── import 'package:flutter/foundation.dart'; Future<int> countWords(String text) => compute(_count, text); int _count(String text) => text.split(RegExp(r'\s+')).length; … - 14
The export takes thirty seconds and needs a progress bar and a working Cancel button — how do you get both out of a spawned isolate?
HardProgress is a
SendPortthe worker writes to as it goes; cancellation only works if the worker actually looks, because nothing outside an isolate can interrupt a running Dart loop except killing it.// Worker: reports progress, and looks for a cancel between chunks. void _exportWorker(SendPort toMain) { final control = ReceivePort(); // a ReceivePort can't be sent — toMain.send(control.sendPort); // the worker makes its own and sends the port var cancelled = false; control.listen((msg) { … - 15
Memory only grows while your export worker runs, and the CPU profiler shows an idle app — how do you profile a spawned isolate?
HardDevTools works one isolate at a time: the CPU profiler, the debugger and every heap snapshot apply to whichever isolate is picked in the isolate selector, and that defaults to the main one.
import 'dart:developer'; import 'dart:io'; import 'dart:isolate'; // ⚠️ per-isolate state: this map is a separate, invisible cache inside every // worker. The main isolate's heap snapshot will never show it growing. … - 16
You moved the work into an isolate and the raster thread still spikes — which threads does a Flutter app have, and who runs your Dart?
HardYour application Dart runs on the engine's UI task runner, which hosts the root isolate; the raster, IO and platform runners execute no app Dart at all, so an isolate can only ever fix a UI-thread problem.
// Everything in build() is UI-runner work, charged to this frame's budget. // ❌ a sync read on the root isolate: the UI runner stops dead final raw = File(path).readAsBytesSync(); final report = Report.fromJson(jsonDecode(utf8.decode(raw))); …