openlang

Isolate.run took my whole Loader with it

Moved a big JSON decode off the main isolate with Isolate.run and got an error about an unsendable object instead of a speedup. The decode itself was fine. The closure was the problem.

class Loader {
  final updates = StreamController<int>();

  List<Object?> _decode(String text) => jsonDecode(text) as List<Object?>;

  Future<List<Object?>> parse(String text) {
    return Isolate.run(() => _decode(text));
  }
}

_decode is an instance method, so the closure holds this, and this holds the StreamController. Everything the closure can reach is sent to the new isolate, and a controller can't be sent. Making _decode a static method fixed it. What I still don't know is how to spot this before running it, because the analyzer is perfectly happy with the code above.

The conversation continues 2

Follow the reasoning
  1. Permalink

    The error message names the class it choked on, and that's the best clue you get. If it says _StreamController or _AsyncCompleter and your function doesn't touch one, something in the closure is holding an object that does. My habit now is to give Isolate.run only top level or static functions and pass every value as an argument. Annoying, but it makes the list of what crosses visible in one line.

  2. Permalink

    I'd also keep the controller update on the main side. Let the isolate return the decoded list, then after the await push to updates from parse. That way the background function stays a plain function from a string to a list, and if the decode later needs to report progress you can decide then whether it's worth a long lived isolate with ports.

Other sides of the same question

  1. A println inside the swap! function

    The docs for swap! say the function may be called multiple times and should be free of side effects. Code in the wild still l…

    Replies 1clojure
  2. Workers stuck on the results send after the context is canceled

    Ran into a simple thing and spent longer on it than I'd like. A pool of eight workers reads tasks from jobs and writes result…

    Replies 8go
  3. asyncio.TaskGroup when one fetch fails

    Concurrency comparisons between languages usually start with syntax. I'd start with what the calculation needs when something…