Irbisa · Cheatsheet
Isolates & True Concurrency
Isolate.run, spawn, ports, message copying, worker pools, TransferableTypedData, DevTools
How do
ReceivePortandSendPortlet two isolates talk, and why is that safer than threads sharing memory?Is Flutter single-threaded? Which threads does a Flutter app run, and which one executes your Dart code?
What is the difference between
compute(),Isolate.run()andIsolate.spawn(), and when do you needspawn?// one argument in, one value outFuture<List<Order>> parse(String body) => compute(parseOrders, body, debugLabel: 'parseOrders'); // a closure: captures several inputs, and may be asyncFuture<Report> buildReport(List<OrderRow> rows, Filter filter) =>…Find the bug: the loading spinner freezes for half a second when the report arrives, even though
loadisasync.class ReportRepository { ReportRepository(this._api); final ReportApi _api; Future<Report> load() async { final body = await _api.fetchReport(); // ~8 MB of JSON return _parse(body); } Future<Report> _parse(String body) async { final json = await Future(() => jsonDecode(body) as Map<String, dynamic>); return Report.fromJson(json); }}What does this code print, and why doesn't the worker see the value the main isolate set?
import 'dart:isolate'; int counter = 0; Future<void> main() async { counter = 7; final fromWorker = await Isolate.run(() { counter += 100; return counter; }); print(fromWorker); print(counter);}This
Isolate.runcall inside aStatethrows "object is unsendable": what is it actually trying to send?class _ReportPageState extends State<ReportPage> { final _scroll = ScrollController(); String _raw = ''; List<OrderRow> _rows = const []; Future<void> _parse() async { final rows = await Isolate.run(() => parseRows(_raw)); setState(() => _rows = rows); } // build() omitted}This worker handshake throws "Bad state: Stream has already been listened to". What is wrong, and how should the handshake look?
Future<SendPort> startWorker() async { final fromWorker = ReceivePort(); await Isolate.spawn(workerMain, fromWorker.sendPort); final toWorker = await fromWorker.first as SendPort; fromWorker.listen((reply) => handleReply(reply)); return toWorker;} void workerMain(SendPort toMain) { final inbox = ReceivePort(); toMain.send(inbox.sendPort); inbox.listen((job) => toMain.send(process(job)));}An exception inside a spawned isolate never reaches your
try/catchand the screen spins forever: where did that error go?Future<Isolate> startImporter(SendPort results, void Function() failAll) { final errors = ReceivePort(); final exits = ReceivePort(); errors.listen((pair) { final [error, stack] = pair as List; // two Strings, never the object reporter.record(error as String, StackTrace.fromString(stack as String? ?? ''));…You call
isolate.kill()on a worker stuck in a two-second loop and it finishes the loop anyway: what doeskillactually promise?void spinner(SendPort toMain) { final sw = Stopwatch()..start(); while (sw.elapsedMilliseconds < 2000) {} // no await: one single event toMain.send('finished');}…Find the bug: every time the user opens and leaves the scan screen, the app's memory and CPU use climb and never come back down.
class _LiveScanState extends State<LiveScan> { final _results = ReceivePort(); final _search = TextEditingController(); int _found = 0; @override void initState() { super.initState(); _results.listen((msg) => setState(() => _found = msg as int)); Isolate.spawn(scanFolder, _results.sendPort, debugName: 'scanner'); } @override void dispose() { _search.dispose(); super.dispose(); }}What does
SendPort.senddo with your object, and why is a 40 MBUint8Listfar slower to send than a 40 MBString?What is an isolate group, and how does
Isolate.exitreturn a big result without copying it?// one-shot worker: the result is handed over, not copiedFuture<Report> buildReport(List<OrderRow> rows) async { final port = ReceivePort(); await Isolate.spawn(_build, (port.sendPort, rows)); return await port.first as Report;}…What does
TransferableTypedDataactually save you, and when is a plainsendof aUint8Listjust as good?// worker: join the encoded chunks once, then move the buffervoid encoderMain(SendPort toMain) { final chunks = <Uint8List>[for (final frame in frames()) encode(frame)]; final moved = TransferableTypedData.fromList(chunks); // the one copy toMain.send(moved); // constant time, whatever the size // moved.materialize(); // would throw ArgumentError: it left with the send…You call
Isolate.runfor every list row and the grid still stutters: when does a long-lived worker beat spawning per task?class Thumbnailer { // handshake as in the spawn example: the worker sends its SendPort first Thumbnailer(this._requests); final SendPort _requests; // reply listener: _pending.remove(id)!.complete(bytes), or completeError(error) final _pending = <int, Completer<Uint8List>>{};…An export takes thirty seconds and needs a progress bar and a working Cancel button: how do you get both out of a spawned isolate?
void exportMain(SendPort toMain) { final control = ReceivePort(); // a ReceivePort cannot be sent, toMain.send(control.sendPort); // so send its SendPort back first var cancelled = false; control.listen((msg) { if (msg == 'cancel') cancelled = true;…One 40-megapixel image ties up your thumbnail pool and the whole grid stops: how do you size and schedule that pool?
compute()on Flutter web returns the right answer and still freezes the tab for two seconds: what is it doing there?// work.dart: one import for every platformexport 'work_io.dart' if (dart.library.js_interop) 'work_web.dart'; // work_io.dart (mobile and desktop): a real isolateFuture<int> countWords(String text) => Isolate.run(() => countWordsSync(text));…Memory keeps growing while your export worker runs, yet the CPU profiler shows an idle app: how do you profile a spawned isolate?