Irbisa · Cheatsheet

Isolates & True Concurrency

Isolate.run, spawn, ports, message copying, worker pools, TransferableTypedData, DevTools

Senior Developer · 18 questions

  1. 01

    How do ReceivePort and SendPort let two isolates talk, and why is that safer than threads sharing memory?

    Each isolate has its own heap and event loop, so no object is ever shared mutably; two isolates talk only by sending copied messages through ports.

  2. 02

    Is Flutter single-threaded? Which threads does a Flutter app run, and which one executes your Dart code?

    Not quite: the engine uses several threads, but your Dart code runs on exactly one, the root isolate.

  3. 03

    What is the difference between compute(), Isolate.run() and Isolate.spawn(), and when do you need spawn?

    compute and Isolate.run are one-shot helpers: start a worker, run one function, return one result, exit; on native platforms Flutter's compute simply calls Isolate.run.

    // 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) =>…
  4. 04

    Find the bug: the loading spinner freezes for half a second when the report arrives, even though load is async.

    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);  }}

    Future(() => ...) moves nothing off the UI isolate: it only queues the callback on the same event loop, so jsonDecode and fromJson still run there as one long event and every frame waits for them.

  5. 05

    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);}

    It prints 100 and then 7.

  6. 06

    This Isolate.run call inside a State throws "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}

    The whole State.

  7. 07

    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)));}

    A ReceivePort is a single-subscription stream: first subscribes, takes one message and cancels, which also closes the port, so the second listen throws.

  8. 08

    An exception inside a spawned isolate never reaches your try/catch and the screen spins forever: where did that error go?

    It killed the worker and was printed to the console.

    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? ?? ''));…
  9. 09

    You call isolate.kill() on a worker stuck in a two-second loop and it finishes the loop anyway: what does kill actually promise?

    kill() defaults to priority: Isolate.beforeNextEvent: stop when control next returns to the event loop.

    void spinner(SendPort toMain) {  final sw = Stopwatch()..start();  while (sw.elapsedMilliseconds < 2000) {} // no await: one single event  toMain.send('finished');}…
  10. 10

    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();  }}

    dispose never closes _results or kills the worker, so every visit leaves a scanner isolate running and an open port whose listener keeps the dead State alive and calls setState on it.

  11. 11

    What does SendPort.send do with your object, and why is a 40 MB Uint8List far slower to send than a 40 MB String?

    send deep-copies the whole object graph synchronously, on the sending isolate.

  12. 12

    What is an isolate group, and how does Isolate.exit return a big result without copying it?

    Since Dart 2.15, Isolate.spawn creates the new isolate in the caller's isolate group: the isolates share the loaded program and one managed heap, though each sees only its own objects.

    // 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;}…
  13. 13

    What does TransferableTypedData actually save you, and when is a plain send of a Uint8List just as good?

    It makes each send a constant-time handover of an off-heap buffer, but TransferableTypedData.fromList copies the bytes once when you create it.

    // 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…
  14. 14

    You call Isolate.run for every list row and the grid still stutters: when does a long-lived worker beat spawning per task?

    When the work per call is small compared with what a spawn costs and throws away.

    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>>{};…
  15. 15

    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?

    Progress is a SendPort the worker writes to as it goes.

    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;…
  16. 16

    One 40-megapixel image ties up your thumbnail pool and the whole grid stops: how do you size and schedule that pool?

    Size the pool from Platform.numberOfProcessors and the memory budget, and let each worker pull the next job from one shared queue when it goes idle.

  17. 17

    compute() on Flutter web returns the right answer and still freezes the tab for two seconds: what is it doing there?

    The web has no Dart isolates.

    // 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));…
  18. 18

    Memory keeps growing while your export worker runs, yet the CPU profiler shows an idle app: how do you profile a spawned isolate?

    DevTools inspects one isolate at a time: the CPU profiler, the debugger and heap snapshots all follow the isolate picked in the isolate selector, which defaults to the main one.