Isolates & Multithreading
Unlike traditional operating system threads that share a single memory space, Dart applications achieve true multithreading using Isolates. An Isolate is an isolated unit of execution with its own private heap memory, garbage collector, and dedicated Event Loop.
1. Shared-Nothing Memory Model
In C++ or Java, threads share memory, requiring synchronization primitives (mutexes, locks, atomic operations) to prevent data races.
Dart eliminates memory locking overhead entirely by implementing a Shared-Nothing Architecture:
- Memory state in one Isolate cannot be accessed or mutated directly by another Isolate.
- Communication between Isolates occurs exclusively through Asynchronous Message Passing over ports (
SendPortandReceivePort).
βββββββββββββββββββββββββββββββ Message Passing βββββββββββββββββββββββββββββββ
β Isolate 1 β (Copy or TransferableData) β Isolate 2 β
β βββββββββββββββββββββββββββ β ββββββββββββββββββββββββββββΊ β βββββββββββββββββββββββββββ β
β β Private Heap Memory β β β β Private Heap Memory β β
β βββββββββββββββββββββββββββ β ββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββ β SendPort / ReceivePort β βββββββββββββββββββββββββββ β
β β Private Event Loop β β β β Private Event Loop β β
β βββββββββββββββββββββββββββ β β βββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββ2. Under The Hood: Message Passing & Zero-Copy Transfers
Deep Memory Copying
When sending standard Dart objects (String, Map, List, custom objects) across a SendPort, the Dart VM serializes and deep-copies the memory structures into the recipient Isolateβs heap.
- Implication: Sending a massive 100MB object across Isolates copies 100MB of memory, causing noticeable CPU spikes and latency!
Zero-Copy Memory Transfers (TransferableTypedData)
To pass large binary data (such as raw image bytes or audio buffers) without copying overhead, Dart provides TransferableTypedData.
- Mechanism: Transfers underlying byte buffer ownership directly from the sender Isolate to the receiver Isolate in $O(1)$ time.
- Post-Transfer State: The senderβs reference to the byte buffer becomes immediately invalid/cleared.
3. Communication Patterns: SendPort, ReceivePort, and compute()
High-Level Worker: Isolate.run() / compute()
For short-lived CPU-bound tasks (e.g. parsing a 20MB JSON string), Flutter provides Isolate.run() (or compute()):
import 'dart:convert';
import 'package:flutter/foundation.dart';
// Top-level or static function
List<User> parseUsers(String rawJson) {
final List parsed = jsonDecode(rawJson);
return parsed.map((json) => User.fromJson(json)).toList();
}
Future<List<User>> loadUsersAsync(String rawJson) async {
// Spawns short-lived isolate, passes payload, returns result, terminates isolate
return await Isolate.run(() => parseUsers(rawJson));
}Long-Lived Worker: ReceivePort and SendPort Handshake
For persistent background workers (e.g. crypto hashing stream, continuous image processing), establish a bidirectional port handshake:
import 'dart:isolate';
class WorkerIsolate {
late SendPort _workerSendPort;
final ReceivePort _receivePort = ReceivePort();
Future<void> spawn() async {
await Isolate.spawn(_entryPoint, _receivePort.sendPort);
// Receive the worker's SendPort in the first message
_workerSendPort = await _receivePort.first as SendPort;
}
static void _entryPoint(SendPort mainSendPort) {
final workerReceivePort = ReceivePort();
// Handshake step 1: send worker's SendPort to main Isolate
mainSendPort.send(workerReceivePort.sendPort);
// Listen for incoming work payloads
workerReceivePort.listen((message) {
final result = _heavyCalculation(message);
mainSendPort.send(result);
});
}
static String _heavyCalculation(dynamic data) => 'Processed $data';
}4. Background Platform Channels (RootIsolateToken)
Historically, background Isolates could not execute Flutter platform channels (such as SharedPreferences or sqflite) because they lacked the main UI Isolateβs binary messenger bindings.
Flutter solved this with RootIsolateToken:
import 'package:flutter/services.dart';
void backgroundIsolateEntry(RootIsolateToken token) {
// Register main Isolate's token with background Isolate's PlatformDispatcher
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
// Now platform channels can be invoked safely from background Isolate!
}5. Trade-offs & Production Considerations
- Spawn Overhead: Spawning an Isolate incurs memory allocation overhead (~100KB-2MB) and startup latency (~5-50ms). Spawning hundreds of temporary Isolates degrades performance. Use long-lived Isolate worker pools for recurring tasks.
- Global State Isolation: Global singletons (like
GetIt,Provider, or static fields) are NOT shared across Isolates. Each Isolate maintains its own uninitialized copy of static variables.