Isolates
Understand how to achieve concurrency using Isolates in Dart.
What is it?
Isolates are Dart's model for concurrent programming. Each isolate runs in its own memory space with its own event loop, communicating only through message passing. Unlike threads that share memory, isolates are completely independent, making them safer and more predictable.
Why does it exist?
Isolates exist to:
- Achieve true parallelism (multiple CPU cores)
- Prevent shared memory issues (race conditions)
- Run expensive computations without blocking UI
- Handle multiple tasks simultaneously
- Improve application performance
- Create responsive applications
Basic Isolates
What is an Isolate?
An Isolate is like a lightweight process that runs independently. Each isolate has its own memory, event loop, and can only communicate via messages.
import 'dart:isolate';
// Function that runs in the isolate
void isolateFunction(SendPort sendPort) {
// Send a message back to the main isolate
sendPort.send('Hello from isolate!');
}
void main() async {
// Create a ReceivePort to receive messages
ReceivePort receivePort = ReceivePort();
// Spawn a new isolate
Isolate isolate = await Isolate.spawn(
isolateFunction,
receivePort.sendPort,
);
// Listen for messages
receivePort.listen((message) {
print('Received: $message');
// Clean up
receivePort.close();
isolate.kill();
});
print('Main isolate continuing...');
}
// Output:
// Main isolate continuing...
// Received: Hello from isolate!
What's happening here? -
Isolate.spawn()creates a new isolate - The isolate runsisolateFunctionin its own memory space -SendPortis used to send messages back to the main isolate -ReceivePortlistens for incoming messages - Isolates communicate via message passing
Two-Way Communication
Isolates can communicate in both directions by exchanging SendPorts.
import 'dart:isolate';
// Worker isolate function
void workerIsolate(SendPort mainSendPort) {
// Create a receive port for this isolate
ReceivePort receivePort = ReceivePort();
// Send our receive port back to main
mainSendPort.send(receivePort.sendPort);
// Listen for messages from main
receivePort.listen((message) {
if (message is String) {
print('Worker received: $message');
// Process the message
String result = 'Processed: ${message.toUpperCase()}';
// Send result back to main
mainSendPort.send(result);
}
});
}
void main() async {
ReceivePort mainReceivePort = ReceivePort();
// Spawn the worker isolate
Isolate isolate = await Isolate.spawn(
workerIsolate,
mainReceivePort.sendPort,
);
// Get the worker's send port
SendPort? workerSendPort;
await for (var message in mainReceivePort) {
if (message is SendPort) {
workerSendPort = message;
break;
}
}
// Send messages to the worker
workerSendPort!.send('Hello');
workerSendPort!.send('World');
// Listen for responses
int received = 0;
mainReceivePort.listen((message) {
if (message is String) {
print('Main received: $message');
received++;
if (received == 2) {
mainReceivePort.close();
isolate.kill();
}
}
});
}
// Output:
// Worker received: Hello
// Worker received: World
// Main received: Processed: HELLO
// Main received: Processed: WORLD
Key insights: - Both isolates exchange SendPorts - Each isolate has its own ReceivePort - Messages are sent asynchronously - Communication is one-way (send and receive) - This enables bidirectional communication
Isolate Communication
Passing Complex Data
Isolates can pass complex data structures, but they must be sendable (primitive types, lists, maps, etc.).
import 'dart:isolate';
// Data classes
class Task {
final int id;
final String name;
final List<int> numbers;
Task(this.id, this.name, this.numbers);
// Convert to map for sending
Map<String, dynamic> toMap() => {
'id': id,
'name': name,
'numbers': numbers,
};
// Create from map
factory Task.fromMap(Map<String, dynamic> map) {
return Task(
map['id'] as int,
map['name'] as String,
(map['numbers'] as List).cast<int>(),
);
}
}
class Result {
final int taskId;
final int sum;
final int count;
Result(this.taskId, this.sum, this.count);
Map<String, dynamic> toMap() => {
'taskId': taskId,
'sum': sum,
'count': count,
};
factory Result.fromMap(Map<String, dynamic> map) {
return Result(
map['taskId'] as int,
map['sum'] as int,
map['count'] as int,
);
}
}
// Worker isolate
void workerIsolate(SendPort mainSendPort) {
ReceivePort receivePort = ReceivePort();
mainSendPort.send(receivePort.sendPort);
receivePort.listen((message) {
if (message is Map<String, dynamic>) {
// Parse the task
var task = Task.fromMap(message);
print('Worker processing task: ${task.name}');
// Compute result
int sum = task.numbers.reduce((a, b) => a + b);
int count = task.numbers.length;
// Send result back
var result = Result(task.id, sum, count);
mainSendPort.send(result.toMap());
}
});
}
void main() async {
ReceivePort mainReceivePort = ReceivePort();
Isolate isolate = await Isolate.spawn(
workerIsolate,
mainReceivePort.sendPort,
);
// Get worker's send port
SendPort? workerSendPort;
await for (var message in mainReceivePort) {
if (message is SendPort) {
workerSendPort = message;
break;
}
}
// Create tasks
var tasks = [
Task(1, 'Sum 1-10', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
Task(2, 'Sum evens', [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]),
Task(3, 'Sum odds', [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]),
];
// Send tasks to worker
for (var task in tasks) {
workerSendPort!.send(task.toMap());
}
// Collect results
int received = 0;
mainReceivePort.listen((message) {
if (message is Map<String, dynamic>) {
var result = Result.fromMap(message);
print('Task ${result.taskId}: Sum = ${result.sum}, Count = ${result.count}');
received++;
if (received == tasks.length) {
mainReceivePort.close();
isolate.kill();
}
}
});
}
// Output:
// Worker processing task: Sum 1-10
// Worker processing task: Sum evens
// Worker processing task: Sum odds
// Task 1: Sum = 55, Count = 10
// Task 2: Sum = 110, Count = 10
// Task 3: Sum = 100, Count = 10
What's happening here? - Complex data is serialized to maps - Tasks are sent to the worker isolate - Worker processes each task - Results are sent back to main - This is how you pass data between isolates
Isolate Lifecycle
Managing Isolates
Isolates have a lifecycle that includes creation, running, and termination.
import 'dart:isolate';
void workerFunction(SendPort sendPort) {
print('Worker: Starting');
// Do some work
for (var i = 1; i <= 5; i++) {
print('Worker: Working... $i');
// Simulate work
}
print('Worker: Finished');
sendPort.send('Done');
}
void main() async {
ReceivePort receivePort = ReceivePort();
// Spawn isolate
print('Main: Creating isolate');
Isolate isolate = await Isolate.spawn(
workerFunction,
receivePort.sendPort,
);
print('Main: Isolate created');
// Handle completion
receivePort.listen((message) {
print('Main: Received: $message');
// Clean up
receivePort.close();
isolate.kill(priority: Isolate.immediate);
print('Main: Isolate killed');
});
}
// Output:
// Main: Creating isolate
// Main: Isolate created
// Worker: Starting
// Worker: Working... 1
// Worker: Working... 2
// Worker: Working... 3
// Worker: Working... 4
// Worker: Working... 5
// Worker: Finished
// Main: Received: Done
// Main: Isolate killed
Key insights: - Isolates are created with
spawn()- They run asynchronously - They can be killed withkill()- Usepriority: Isolate.immediatefor immediate termination - Always clean up resources
Real-World Examples
Parallel Processing
Isolates are perfect for CPU-intensive operations that would block the UI.
import 'dart:isolate';
// CPU-intensive task: Fibonacci calculation
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Worker isolate for Fibonacci
void fibWorker(SendPort sendPort) {
ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((message) {
if (message is int) {
print('Worker: Calculating Fibonacci of $message');
int result = fibonacci(message);
sendPort.send(result);
}
});
}
void main() async {
ReceivePort mainPort = ReceivePort();
Isolate isolate = await Isolate.spawn(fibWorker, mainPort.sendPort);
// Get worker's send port
SendPort? workerPort;
await for (var message in mainPort) {
if (message is SendPort) {
workerPort = message;
break;
}
}
// Calculate Fibonacci numbers in parallel
List<int> numbers = [35, 36, 37, 38, 39, 40];
Map<int, Future<int>> futures = {};
// Send each number to the worker
for (var n in numbers) {
var completer = Completer<int>();
futures[n] = completer.future;
// Store completer for when result arrives
workerPort!.send(n);
}
// Collect results
int received = 0;
mainPort.listen((message) {
if (message is int) {
received++;
print('Result: $message');
if (received == numbers.length) {
mainPort.close();
isolate.kill();
}
}
});
}
Load Balancer
Multiple isolates can be used to create a load balancer for parallel processing.
import 'dart:isolate';
class LoadBalancer {
final List<SendPort> _workers = [];
final List<bool> _isBusy = [];
final List<Completer> _completers = [];
LoadBalancer(int workerCount) {
_initWorkers(workerCount);
}
Future<void> _initWorkers(int count) async {
for (var i = 0; i < count; i++) {
final completer = Completer<SendPort>();
// Spawn worker
Isolate.spawn(_workerFunction, completer);
// Wait for worker to be ready
final workerPort = await completer.future;
_workers.add(workerPort);
_isBusy.add(false);
}
}
static void _workerFunction(SendPort mainPort) {
ReceivePort workerPort = ReceivePort();
mainPort.send(workerPort.sendPort);
workerPort.listen((message) {
// Process the work
// Send result back
});
}
Future<dynamic> execute(dynamic task) async {
// Find free worker
int index = _isBusy.indexOf(false);
if (index == -1) {
// Wait for a worker to become free
// Implementation for waiting
}
_isBusy[index] = true;
final completer = Completer();
_completers.add(completer);
// Send task to worker
_workers[index].send(task);
return completer.future;
}
}
Best Practices
Use Isolates for CPU-Intensive Work
// Good: Use isolate for heavy computation
void heavyComputation() {
Isolate.spawn(computeTask, sendPort);
}
// Bad: Block the main isolate with heavy computation
void heavyComputationBlocking() {
// This will block the UI
for (var i = 0; i < 1000000000; i++) {
// Heavy computation
}
}
Always Clean Up
// Good: Clean up resources
void useIsolate() async {
var receivePort = ReceivePort();
var isolate = await Isolate.spawn(worker, receivePort.sendPort);
// Use the isolate...
// Clean up
receivePort.close();
isolate.kill();
}
// Bad: Leaking resources
void badUseIsolate() async {
var receivePort = ReceivePort();
var isolate = await Isolate.spawn(worker, receivePort.sendPort);
// Never cleaned up!
}
Common Mistakes
Forgetting to Close ReceivePort
Wrong:
void main() async {
var receivePort = ReceivePort();
await Isolate.spawn(worker, receivePort.sendPort);
// ReceivePort never closed
}
Correct:
void main() async {
var receivePort = ReceivePort();
var isolate = await Isolate.spawn(worker, receivePort.sendPort);
// Use the isolate...
receivePort.close();
isolate.kill();
}
Passing Non-Sendable Data
Wrong:
void worker(SendPort sendPort) {
// Can't pass functions or class instances
sendPort.send(() => print('Hello')); // Error!
}
Correct:
void worker(SendPort sendPort) {
// Pass only sendable types
sendPort.send('Hello'); // Works
sendPort.send(42); // Works
sendPort.send({'key': 'value'}); // Works
}
Summary
Isolates provide safe, concurrent execution in Dart. They run independently with their own memory and event loops, communicating only through message passing.
Next Steps
Now that you understand isolates, continue to:
Did You Know?
- Isolates don't share memory
- Each isolate has its own event loop
- Isolates can use multiple CPU cores
- Spawning isolates has some overhead
- Isolates communicate via message passing
- You can pause, resume, and kill isolates
- Isolates are used in Flutter for background tasks