concurrent or parallel
Two ways to overlap work. They are not the same operator with two names.
Lecture
concurrent(n) overlaps
Futures on this isolate — I/O, waiting.
parallel(n, worker) overlaps
CPU work across other isolates. Pick by what the callback
spends its time doing, not by how much you want to "go faster."
concurrent(n) | parallel(n) | |
|---|---|---|
| What overlaps | Futures on this isolate | worker isolates |
| Callback | any closure | top-level or static function |
| Values | anything | sendable |
| Platforms | VM, Flutter, web | VM / Flutter only |
Cheap work (x + 1, Future.delayed(0)) | a marker; hops are cheap | an isolate message per item — usually a loss |
| The right job | HTTP, DB, files, await | JSON parse, images, crypto, a tight loop |
I/O is mostly waiting. While a request is in flight the isolate is
idle, so overlapping four Futures with
concurrent(4) cuts wall-clock time and does not need
another isolate. CPU work is the isolate: it blocks the
event loop (and a Flutter frame) until it returns. That is what
parallel is for.
The isolate hop is not free. Each item is serialized, sent, run,
serialized back, and reordered. A callback whose body is
x + 1 spends more time in that hop than in the
addition — four workers then make it slower than one,
because you pay four times the postage for no CPU to reclaim.
Measured: a thousand x + 1s on four workers took
about twice as long as on one; a tight 20000-iteration loop
on four workers was about twice as fast as on one. If the
work is not heavier than the hop, stay on this isolate.
// I/O — overlap Futures here
fx(ids).mapConcurrent(8, fetchUser);
// CPU — only when the body is heavy enough
fx(blobs).parallel(parallelWorkers, parseJson);
// This is a loss. The hop is bigger than the work.
fx(nums).parallel(4, (x) => x + 1);
concurrent — I/O, any closure ·
mapConcurrent — the combined I/O form ·
parallel — CPU, sendable worker ·
mapParallel — alias of parallel