Skip to main content

Using Isolates for Concurrency

1. Understanding Dart Isolates

Aspect Description Details
Isolate Definition An isolate in Dart is a separate execution thread with its own memory space, ensuring that variables and objects are not shared between isolates.
Isolate Memory Isolation Each isolate has its own heap and memory space, preventing direct access to another isolate's memory.
Isolate Communication Isolates communicate through message passing using SendPort and ReceivePort.
Isolate Thread Safety Isolates provide thread safety by ensuring that no shared mutable state exists between them.
Isolate Performance Isolates can be used to perform CPU-intensive tasks without blocking the main UI thread.

Example: Creating an Isolate

// Import the dart:isolate library
import 'dart:isolate';

void main() async {
  // Create a receive port to listen for messages
  ReceivePort receivePort = ReceivePort();
  
  // Spawn a new isolate with a function and send port
  await Isolate.spawn(isolateFunction, receivePort.sendPort);
  
  // Listen for messages from the isolate
  receivePort.listen((message) {
    print('Received: $message');
  });
}

// Function to run in a separate isolate
void isolateFunction(SendPort sendPort) {
  String result = 'Hello from Isolate';
  sendPort.send(result);
}
Note: Isolates in Dart are not threads but separate execution contexts. They do not share memory, and communication happens through message passing.

2. Creating Isolates

Method Description Parameters Notes
Isolate.spawn Creates and runs a new isolate isolateEntry, message, debugName Entry point function and optional message to pass
Isolate.spawnUri Spawns an isolate from a URI uri, message, debugName Useful for loading code from a separate file

Example: Creating an Isolate

import 'dart:isolate';

void main() async {
  final isolate = await Isolate.spawn(myIsolateFunction, 'Hello from main');
  print('Isolate created successfully');
}

void myIsolateFunction(String message) {
  print('Received in isolate: $message');
}
Note: Isolates are isolated and cannot directly share memory. Communication must be done via message passing.

3. Using compute() Function

The compute() function in Flutter is a utility for running CPU-intensive tasks on background isolates, allowing you to keep the UI responsive. It's a simple way to offload work from the main isolate.

Parameter Type Description
compute ComputeFunction<T, R> The function to run on a background isolate. It must be a top-level or static function.
input T The input data to pass to the function. Must be serializable.

Example: Using compute() to process a list of numbers

import 'package:flutter/foundation.dart';

int sumList(List<int> numbers) {
  return numbers.reduce((a, b) => a + b);
}

Future<int> processNumbers(List<int> numbers) async {
  final result = await compute(sumList, numbers);
  return result;
}
Note: The function passed to compute() must be a top-level or static function, not a method of a class. This is because the function needs to be serializable and sent to another isolate.
Warning: The compute() function is not suitable for I/O operations. Use it only for CPU-bound tasks.

4. Communicating Between Isolates

Isolates in Flutter communicate through message passing using SendPort and ReceivePort. Messages are serialized and sent between isolates, making them safe for concurrent execution.

Class Description Usage
ReceivePort Receives messages from other isolates. Used to listen for incoming messages in an isolate.
SendPort Sends messages to another isolate. Provided by ReceivePort to send messages to a specific isolate.

Example: Sending and Receiving Messages

// Main isolate
import 'dart:isolate';

void main() async {
  final receivePort = ReceivePort();
  final sendPort = await Isolate.spawn(isolateFunction, receivePort.sendPort);

  receivePort.listen((message) {
    print('Received: $message');
  });

  sendPort.send('Hello from main isolate');
}

void isolateFunction(SendPort sendPort) {
  final receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);

  receivePort.listen((message) {
    print('Isolate received: $message');
    sendPort.send('Hello from isolate');
  });
}
Note: Isolates do not share memory, so all data must be serialized when passed between them.

Key Points

  • Use ReceivePort to listen for messages in an isolate.
  • Use SendPort to send messages to another isolate.
  • Messages are serialized, so only serializable data types can be sent.

5. Handling Isolate Errors

Error Type Description Handling Strategy
IsolateSpawnException Thrown when an isolate fails to spawn. Wrap in try-catch and log the error details.
IsolateShutdownException Occurs when an isolate shuts down unexpectedly. Use Isolate.exit() with a reason and handle in onExit callback.
SendPortException Occurs when sending messages to a dead isolate. Check isolate.isRunning before sending messages.

Example: Catching Isolate Errors

import 'dart:isolate';

void main() async {
  try {
    final isolate = await Isolate.spawn(someFunction, 'data');
  } catch (e) {
    print('Isolate spawn error: $e');
  }
}

void someFunction(String data) {
  // Simulate an error
  throw Exception('Isolate error occurred');
}
Note: Errors in isolates do not propagate to the main isolate. Always handle errors explicitly within the isolate or use Isolate.exit() to signal failure.

6. Using IsolateRunner for Reusable Isolates

Example: Basic IsolateRunner Usage

// Define a function to run in isolate
Future<String> isolateTask(String input) async {
  // Simulate CPU-intensive work
  await Future.delayed(Duration(seconds: 2));
  return 'Processed: $input';
}

// Create IsolateRunner instance
final isolateRunner = IsolateRunner(
  isolateTask,
  maxConcurrent: 3,
);

// Run tasks
final results = await Future.wait([
  isolateRunner.run('task1'),
  isolateRunner.run('task2'),
  isolateRunner.run('task3'),
]);

print(results); // [Processed: task1, Processed: task2, Processed: task3]
Property Description Default Value
isolateTask Function to execute in isolate null
maxConcurrent Maximum number of concurrent isolates 1
timeout Timeout for isolate execution (ms) 60000
Note: IsolateRunner manages isolate lifecycle and reuse, reducing overhead from creating new isolates for each task.
Warning: IsolateRunner requires that the task function is serializable and can be sent to a separate isolate.

7. Implementing Background Processing with Isolates

Example: Basic Isolate for Background Task

// Main isolate
import 'dart:isolate';

void main() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(backgroundTask, receivePort.sendPort);

  receivePort.listen((message) {
    print('Result from isolate: $message');
    receivePort.close();
  });
}

// Background task in separate isolate
void backgroundTask(SendPort sendPort) {
  // Simulate heavy computation
  int result = 0;
  for (int i = 0; i < 1000000; i++) {
    result += i;
  }
  sendPort.send(result);
}
Method Description Notes
Isolate.spawn() Creates and runs a new isolate First parameter is the entry point function, second is the message to pass
ReceivePort.listen() Listens for messages from an isolate Can be used to receive results or signals from background tasks
SendPort.send() Sends a message to another isolate Used for communication between isolates
Note: Isolates are isolated from each other and cannot directly share memory. Communication must be done through message passing.

Example: Isolate with Error Handling

import 'dart:isolate';

void main() async {
  final receivePort = ReceivePort();
  try {
    await Isolate.spawn(backgroundTaskWithError, receivePort.sendPort);
  } catch (e) {
    print('Failed to spawn isolate: $e');
  }

  receivePort.listen((message) {
    if (message is String && message.startsWith('Error:')) {
      print('Isolate error: $message');
    } else {
      print('Result from isolate: $message');
    }
    receivePort.close();
  });
}

void backgroundTaskWithError(SendPort sendPort) {
  try {
    // Simulate a long-running task
    int result = 0;
    for (int i = 0; i < 1000000; i++) {
      result += i;
    }
    sendPort.send(result);
  } catch (e) {
    sendPort.send('Error: $e');
  }
}

8. Sharing Data Between Isolates

Method Description Notes
Isolate.spawn() Spawns a new isolate and passes data to it via the message parameter. Only supports JSON-serializable data or IsolateData objects.
Isolate.spawnUri() Spawns an isolate from a URI and passes data via the message parameter. Useful for isolating code in separate files or packages.

Example: Passing Data to Isolate

import 'dart:isolate';

void main() async {
  final data = {'message': 'Hello from main isolate'};
  final receivePort = ReceivePort();
  await Isolate.spawn(isolateFunction, data);
}

void isolateFunction(Map<String, dynamic> data) {
  print(data['message']); // Output: Hello from main isolate
}
Note: Data passed to isolates must be serializable. Use IsolateData for complex data structures.
Serialization Limitation Supported Types Notes
Basic Types String, int, double, bool, List<dynamic> These can be directly passed to isolates.
Complex Types Map<String, dynamic>, DateTime Must be converted to JSON-compatible format before passing.
Custom Objects IsolateData Use IsolateData to wrap custom objects for sharing.

Example: Using IsolateData

import 'dart:isolate';

class MyData {
  final String name;
  final int value;

  MyData(this.name, this.value);
}

void main() async {
  final data = IsolateData(MyData('test', 42));
  await Isolate.spawn(isolateFunction, data);
}

void isolateFunction(IsolateData data) {
  final myData = data.data as MyData;
  print(myData.name); // Output: test
}
Warning: Isolates do not share memory. Communication must be done via message passing.

9. Managing Isolate Lifecycle

Method Description Return Type Notes
Isolate.spawn() Creates and starts a new isolate Future<Isolate> Must be passed a function that accepts a SendPort as its only argument
Isolate.kill() Terminates an isolate Future<bool> Returns true if the isolate was successfully terminated
Isolate.exit() Gracefully exits the current isolate void Must be called from within the isolate to exit it

Example: Spawning and Managing Isolates

// Main isolate
import 'dart:isolate';

void main() async {
  final receivePort = ReceivePort();
  final isolate = await Isolate.spawn(isolateEntryPoint, receivePort.sendPort);

  // Listen for messages from the isolate
  receivePort.listen((message) {
    print('Received: $message');
    isolate.kill(); // Terminate the isolate after receiving a message
  });
}

// Isolate entry point
void isolateEntryPoint(SendPort sendPort) {
  // Do some work here...
  sendPort.send('Hello from isolate');
  Isolate.exit(); // Exit the isolate after sending message
}
Note: Isolates are isolated and do not share memory. Communication between isolates must be done via message passing.
Warning: Isolates do not support shared memory. Any data passed between isolates must be serializable.

10. Optimizing Heavy Computations

Example: CPU-intensive calculation using Isolate

// Main isolate
import 'dart:isolate';

Future<int> calculateSum(List<int> numbers) async {
  final receivePort = ReceivePort();
  final isolate = await Isolate.spawn(
    _calculateSumWorker,
    receivePort.sendPort,
  );

  receivePort.send(numbers);
  final result = await receivePort.first;
  isolate.kill();

  return result;
}

// Worker isolate
void _calculateSumWorker(SendPort sendPort) {
  final receivePort = ReceivePort();
  receivePort.listen((message) {
    final numbers = message as List<int>;
    int sum = 0;
    for (int i = 0; i < numbers.length; i++) {
      sum += numbers[i];
    }
    sendPort.send(sum);
  });
}
Feature Description Notes
Isolate.spawn() Creates a new isolate Can be used to run CPU-intensive tasks without blocking the UI
ReceivePort Receives messages from other isolates Used to communicate between main isolate and worker isolate
SendPort Sends messages to other isolates Used to send data to worker isolate and receive results
Note: Isolates are not threads, they are separate execution contexts with their own memory space.
Warning: Isolates do not share memory, so data must be serialized when passed between isolates.

Was this article helpful?