Эта страница ещё не переведена, поэтому показана на английском. Помогите с переводом

parallel

Overlaps CPU work across isolates, in source order. Not concurrent with a different name.

int get parallelWorkers FxAsyncIterable<R> parallel<A, R>(int workers, FutureOr<R> Function(A input) worker, Iterable<A> iterable, {int chunk = 1, bool chunked = false}) FxAsyncIterable<R> parallelAsync<A, R>(int workers, FutureOr<R> Function(A input) worker, FxAsyncIterable<A> iterable, {int chunk = 1, bool chunked = false}) FxAsyncIterable<R> mapParallel<A, R>(int workers, FutureOr<R> Function(A input) worker, Iterable<A> iterable, {int chunk = 1, bool chunked = false}) FxAsyncIterable<R> mapParallelAsync<A, R>(int workers, FutureOr<R> Function(A input) worker, FxAsyncIterable<A> iterable, {int chunk = 1, bool chunked = false}) fxPipe2(decodePng, thumbnail) // one hop, see fxPipe IsolatePool.spawn(int workers) / IsolatePool.using(int workers, body) FxAsyncIterable<R> parallelOn<A, R>(IsolatePool pool, FutureOr<R> Function(A input) worker, Iterable<A> iterable, {int chunk = 1, bool chunked = false}) FxAsync<R> Fx.parallel(int workers, FutureOr<R> Function(T input) worker, {int chunk = 1, bool chunked = false}) FxAsync<R> Fx.parallelOn(IsolatePool pool, FutureOr<R> Function(T input) worker, {int chunk = 1, bool chunked = false}) FxAsync<R> FxAsync.parallel(int workers, FutureOr<R> Function(T input) worker, {int chunk = 1, bool chunked = false}) FxAsync<R> FxAsync.parallelOn(IsolatePool pool, FutureOr<R> Function(T input) worker, {int chunk = 1, bool chunked = false})

Lecture

concurrent(n) overlaps Futures on the same isolate — I/O. Dart's CPU-bound story is isolates. parallel(n, worker) is the twin: a reused pool of n isolates, results in source order, like mapConcurrent is the combined form of map-plus-concurrent. They are not the same operator — the comparison lives on concurrent or parallel.

Prefer a top-level or static function. A closure that captures a non-sendable (a ReceivePort, an open socket) throws ArgumentError at spawn — the isolate contract, not a fxdart invention. An unsendable input or result fails that pull the same way, rather than hanging. On the web the operator throws UnsupportedError — use concurrent(n) there. This listing is VM-only and is not a live playground. The worker may return a Future (FutureOr, same shape as mapConcurrent) — a sync callback is still the fast path. Nested parallel inside an async worker is allowed: that isolate spawns its own pool, and cancel of the outer chain shuts the nested pool down. One level of nesting is the contract — a third nested parallel is killed with its parent, so it cannot shut down its children.

Don't want to pick n? parallelWorkers is the VM's processor count — pass it as the first argument. A List shorter than n sizes the pool to the list, so parallel(8, w) over two items starts two isolates, not eight. People coming from mapConcurrent can write mapParallel; it is the same operator.

int timesTen(int x) => x * 10;

Future<void> main() async {
  print(await fx([1, 2, 3, 4]).parallel(2, timesTen).toList());
  // [10, 20, 30, 40]
}

chunk — how many elements ride one message

By default every element crosses to a worker on its own. That round trip costs about 5µs, which is more than most callbacks cost, and it is the whole reason a cheap worker is slower under parallel than in a plain loop. chunk: k pays it once per k elements:

// 20,000 elements, ~0.4µs of work each, 4 workers:
await fx(rows).parallel(4, parseRow).toList();             // ~142ms
await fx(rows).parallel(4, parseRow, chunk: 512).toList(); //   ~3ms

// the same work in a plain loop, no isolates:            //   ~8ms

47× on that shape, and the batched form is the first one that actually beats the loop it replaced. Size k so that k × callback is comfortably more than 5µs, while still leaving several batches per worker to balance across — length ~/ (workers * 4) is a fine starting point.

A batch does not change what you observe: order is the same, back-pressure is the same, and a worker that throws still emits the results of the elements before it and then raises on the element that actually failed. Two things do change. The first element now waits for its whole batch, so a take(1) wants a small chunk or none. And an unsendable input or result fails its whole batch rather than only its own pull — finding which element was at fault would mean sending them separately, which is the cost the batch exists to avoid.

Don't want to write chunk: n ~/ (workers * 4) and repeat the worker count? chunked: true does that from the source length:

await fx(rows).parallel(4, parseRow, chunked: true);
// k = rows.length ~/ 16 — one 4, not two

The source must be a List. A generator or an async source has no length — pass chunk: k instead. chunk: and chunked: together throw; the call has one policy.

Two CPU stages, one hop

Two .parallel calls copy every result back to this isolate and out again. Compose the workers with fxPipe2 so both stages run on the worker:

await fx(blobs)
    .parallel(4, fxPipe2(decodePng, thumbnail), chunk: 64)
    .toList();

decodePng and thumbnail must be sendable, same as any parallel worker. The returned function captures both. Add .then for more stages — no arity cap. The last .then is the worker.

Reuse the pool

parallel spawns on the first pull and kills the isolates when that chain ends. Two jobs then pay startup twice. IsolatePool is the spawn-once bracket. IsolatePool.using kills in finally, even if the body throws. Cancel of one parallelOn chain does not kill the pool — the next chain can use it.

await IsolatePool.using(4, (pool) async {
  final a = await fx(batchA).parallelOn(pool, parseRow, chunk: 256).toList();
  final b = await fx(batchB).parallelOn(pool, parseRow, chunk: 256).toList();
  return (a, b);
});
Related: concurrent — I/O, any closure · mapConcurrent — the combined I/O form · concurrent or parallel — I/O vs CPU · mapParallel — the same operator as parallel · fxPipe — compose workers so two stages pay one hop · is parallel worth it? — the same job five ways, measured