Course outline

Dart / Integration: targets, resources, validation, and evidence / Lesson 13

Separate CPU work with isolate and message boundaries

Reason about isolated memory, transferable messages, Isolate.run, port lifetimes, and target-specific concurrency limits.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish isolate execution from async waiting on one event loop.
  • Design small message contracts and an owner for every worker, port, result, and failure.

An isolate is a memory and execution boundary

Dart isolates have separate mutable state and event loops rather than sharing an ordinary mutable object graph. They communicate through supported message mechanisms. This avoids some shared-memory races but introduces copying, transfer, coordination, and lifetime costs. async does not create an isolate, and creating many Futures on one isolate does not make a CPU loop parallel. Use another isolate for justified compute separation on a supporting target, not as a universal optimization. Browser concurrency has different facilities and constraints; the dart:isolate examples here are native-VM-oriented and must not be advertised as browser-compatible programs.

Use a bounded one-shot computation to explain the boundary

Isolate.run runs the supplied computation in a new isolate and returns a Future for its result. Its API has been available since Dart 2.19; this course otherwise uses Dart 3 language features where stated. The example sums three fixed integers, so spawning overhead would dominate useful work in a real implementation. It demonstrates result ownership, not a benchmark or a recommended worker granularity. The computation and returned value must satisfy the API’s transfer requirements. No Dart runtime exists in the authoring environment, so no isolate, process, or timing experiment was actually started.

dart A native-target one-shot isolate computation
import 'dart:async';
import 'dart:isolate';
int sumValues(List<int> values) => values.fold(0, (total, value) => total + value);
Future<void> main() async {
  final result = await Isolate.run(() => sumValues(<int>[1, 2, 3]));
  print(result);
}
Expected output
6

Expected output is 6 on a compatible supporting Dart VM. The input is fixed and small; no parallel speedup, memory-transfer measurement, or execution result is claimed.

Send data with explicit identity and shape

Message rules depend on the isolate API and whether workers share code. Some objects cannot be sent, and capturing a closure can retain more surrounding state than expected, including an unsendable resource. Prefer small explicit payloads over passing an application object that contains files, ports, clients, or unrelated state. A long-lived worker protocol should distinguish request identity, success, rejection, and worker failure. Validate received data before treating it as trusted domain state. Passing a SendPort grants a communication capability; distribute it according to the intended ownership rather than assuming a message channel enforces all application authorization.

Close a port when its owner finishes

ReceivePort exposes a stream of incoming messages and a sendPort for delivery. It is a single-subscription source and can buffer messages until a listener attaches. The following example sends a message to a port in the same isolate, then closes the port in finally. It intentionally does not create a worker: the point is that message delivery and worker creation are separate mechanisms. For a real worker, define readiness, shutdown, error observation, and pending-request completion. A printed response alone does not prove that all worker resources or outstanding requests have been released.

dart A locally owned port lifetime
import 'dart:async';
import 'dart:isolate';
Future<void> main() async {
  final port = ReceivePort();
  try {
    port.sendPort.send('ready');
    print(await port.first);
  } finally {
    port.close();
  }
}
Expected output
ready

Expected output is ready on a supporting native target. This is a message round trip in one isolate, not evidence of a second worker or CPU parallelism. The program was not executed.

Bound worker creation and distinguish cancellation from abandonment

A worker pool needs a bounded admission policy, a result map, and clear behavior when the requester stops waiting. A Future timeout does not automatically terminate the computation or resolve external effects. Explicit isolate termination can also discard work, so decide which tasks may be abandoned and how pending callers learn the outcome. Avoid spawning one isolate per tiny item without measuring transfer and startup costs in an authorized isolated benchmark. Report target, SDK, workload, and actual observations when such evidence exists. Here, only source and API semantics were reviewed; no worker reliability or performance guarantee was tested.

Common errors and fixes

An async CPU loop is expected to execute in a second isolate.

Why it happens
Asynchronous result syntax was confused with isolated execution.
How to fix it
Choose an explicit supported isolate boundary when compute separation is justified.
Prevent it next time
Identify event-loop work and worker work separately in the design.

A worker message fails because a captured object cannot be sent.

Why it happens
The closure retained more surrounding state than its visible calculation needed.
How to fix it
Pass small sendable input explicitly and isolate the computation from resource owners.
Prevent it next time
Review closure captures and message API restrictions before worker extraction.

An unused port or worker keeps application work alive unexpectedly.

Why it happens
Lifecycle completion and shutdown ownership were not defined.
How to fix it
Close owned ports and implement explicit worker shutdown and pending-request policy.
Prevent it next time
Include failure and requester-abandonment paths in lifecycle review.

Check your understanding

1. Does sending one message through ReceivePort prove CPU parallelism?

Show worked solution

No. A port can send and receive within the same isolate, as the example does. Parallel computation requires a supported separate execution boundary and actual work assigned to it. Even then, a useful speedup needs measurement of the workload and coordination costs. Communication syntax alone cannot establish a performance result.

2. A batch worker result arrives after its caller timed out. What must the protocol decide?

Show worked solution

Decide whether to retain the result for later lookup, discard a safe pure-computation result, or reconcile a consequential operation. Keep request identity and error ownership explicit, and bound retained state. Do not interpret the timeout as proof that the worker stopped or that a committed external action never happened.

Key takeaways

  • Isolates exchange messages instead of sharing ordinary mutable state.
  • Execution, message transfer, target support, and lifecycle ownership need separate contracts.

Official references