Dart Concurrency : Isolates

Jinali GhoghariJinali Ghoghari
8 min read

Isolates

Imagine you have a large garden with several gardeners tending to different areas. Each gardener works independently, planting seeds, watering plants, and pruning bushes. They don't interfere with each other's work and communicate through written notes.

In this analogy:

  • The garden represents your computer's processor, capable of handling multiple tasks simultaneously.

  • Each gardener represents an isolate in Dart.

  • Planting seeds, watering plants, and pruning bushes represent different tasks or computations.

  • The written notes represent messages passed between isolates for communication.

Just like in the garden, isolates in Dart allow for multiple tasks to be performed concurrently without interfering with each other. Each isolate works independently in its own "area" (memory space), achieving true concurrency without shared memory access issues.


Unresponsive Applications

Let's simplify it:

Imagine you're cooking in your kitchen. You have a big pot on the stove, and you need to stir it continuously for a long time to make a delicious soup. While you're stirring, you also need to chop vegetables, check on other dishes, and do other kitchen tasks.

Now, if you spend too much time stirring the pot, you might not get a chance to do the other tasks. This could make your kitchen feel slow and unresponsive, and you might not be able to prepare all your dishes efficiently.

In this analogy:

  • The big pot on the stove represents a long-running task, like processing a lot of data or doing complex calculations.

  • Stirring the pot continuously represents doing that task in the main part of your program, called the main isolate.

  • Chopping vegetables and other kitchen tasks represent other things your program needs to do, like updating the user interface or responding to user input.

    To avoid the kitchen feeling slow and unresponsive, you could ask someone else to help you stir the pot while you focus on the other tasks. This way, your kitchen can stay busy and efficient, and you can serve up a delicious meal without making your guests wait too long.

    In programming terms, this is like offloading intensive tasks to background isolates. By doing this, your application can stay responsive and provide a smoother experience for the user, even when there are long-running tasks happening behind the scenes.

import 'dart:isolate';

// Function to simulate a long-running task
void longRunningTask() {
  // Simulate a long-running task
  for (int i = 0; i < 1000000000; i++) {
    // Perform some computation
  }
}

void main() {
  print('Starting main isolate');

  // Call the longRunningTask function
  longRunningTask();

  print('Main isolate completed');
}

Dart Isolate: One-Way Isolate Communication

Dart isolates communicate with each other via message passing.

One-way communication involves sending messages from one isolate to another using SendPort and ReceivePort. This allows isolates to exchange data and coordinate tasks asynchronously.

import 'dart:isolate';

// Function running inside the isolate
void isolateFunction(SendPort sendPort) {
  // Send a message back to the main isolate
  sendPort.send('Hello from isolate!');
}

void main() {
  // Create a ReceivePort to receive messages from the isolate
  ReceivePort receivePort = ReceivePort();

  // Spawn a new isolate and pass the sendPort of the ReceivePort
  Isolate.spawn(isolateFunction, receivePort.sendPort);

  // Listen for messages sent from the isolate
  receivePort.listen((message) {
    print('Received message: $message');
  });
}

Two-Way Isolate Communication

Two-way communication between isolates means they can send messages back and forth. This allows them to share information, coordinate tasks, and collaborate on complex computations.

import 'dart:isolate';

// Function running inside the isolate
void isolateFunction(SendPort sendPort) {
  // Create a new ReceivePort to listen for messages from the main isolate
  ReceivePort receivePort = ReceivePort();

  // Listen for messages sent from the main isolate
  receivePort.listen((message) {
    // Print the message received from the main isolate
    print('Isolate received message: $message');

    // Send a response message back to the main isolate
    sendPort.send('Message received');
  });
}

void main() {
  // Create a ReceivePort to listen for messages from the isolate
  ReceivePort receivePort = ReceivePort();

  // Spawn a new isolate and pass the sendPort of the ReceivePort
  Isolate.spawn(isolateFunction, receivePort.sendPort);

  // Listen for messages sent from the isolate
  receivePort.listen((message) {
    // Print the message received from the isolate
    print('Main isolate received message: $message');
  });
}

Dart: Concurrency | Isolates Lifecycle

In Dart programming, isolates provide a powerful concurrency model by allowing tasks to run independently in separate memory spaces. Understanding the lifecycle of isolates is essential for managing their creation, execution, and termination effectively. In this article, we'll delve into the Dart isolate lifecycle, exploring the stages isolates go through from creation to termination.

1. Creation:

Isolates are created using the Isolate.spawn function, which takes a function as an argument. This function represents the code that will run inside the newly spawned isolate. When an isolate is spawned, it initializes its own memory space and begins executing the provided function independently of the main isolate.

2. Execution:

Once spawned, isolates execute the function passed to Isolate.spawn within their own memory space. They can perform computations, handle events, and communicate with other isolates using message passing. Isolates continue to execute their code until they reach the end of the function or encounter an error.

3. Suspension:

Isolates can be suspended or paused using the Isolate.pause method. When an isolate is paused, it stops executing its code temporarily, allowing other isolates or the main isolate to take precedence. Pausing isolates can be useful for resource management or prioritizing tasks in a concurrent application.

4. Resumption:

Suspended isolates can be resumed using the Isolate.resume method, allowing them to continue executing their code from where they were paused. Resuming an isolate restores its execution state, allowing it to pick up where it left off without losing any progress.

5. Termination:

Isolates terminate when there are no more active ports listening for messages. Once an isolate completes its task or reaches the end of its execution, it automatically terminates if there are no more active ports. Additionally, isolates can be explicitly terminated using the Isolate.kill method, which stops their execution and releases their resources.

import 'dart:isolate';

void isolateFunction(SendPort sendPort) {
  print('Isolate started');
}

void main() {
  Isolate.spawn(isolateFunction, null);
  print('Main isolate executing');
}

Isolates Event Handling

Isolates can handle events asynchronously, allowing them to respond to external stimuli such as user input, network events, or timers.

import 'dart:async';
import 'dart:isolate';

// Function running inside the isolate
void isolateFunction(SendPort sendPort) {
  // Set up a timer to send a message every second
  Timer.periodic(Duration(seconds: 1),(Timer) {
    sendPort.send('Tick');
  });
}

void main() {
  // Create a ReceivePort to listen for messages from the isolate
  ReceivePort receivePort = ReceivePort();

  // Spawn a new isolate and pass the sendPort of the ReceivePort
  Isolate.spawn(isolateFunction, receivePort.sendPort);

  // Listen for messages sent from the isolate
  receivePort.listen((message) {
    print('Received event: $message');
  });
}

The receivePort listens for messages sent from the isolate, and when a message is received, it prints the message, indicating that an event ('Tick') was received from the isolate.


Isolates Background Workers

Isolates are commonly used as background workers for performing CPU-intensive or long-running tasks, such as data processing, image manipulation, or file operations.

By offloading these tasks to background isolates, applications can maintain responsiveness and multitask effectively.

import 'dart:isolate';

// Function representing a CPU-intensive or long-running task
void backgroundTask() {
  // Perform CPU-intensive or long-running task here
}

void main() {
  // Spawn a new isolate to execute the backgroundTask function
  Isolate.spawn(backgroundTask, null);

  // Print a message indicating that the main isolate is executing
  print('Main isolate executing');
}

Using Isolates

You should use isolates whenever your application is handling computations that are large enough to temporarily block other computations.

There aren't any rules about when you must use isolates, but here are some more situations where they can be useful:

  • Parsing and decoding exceptionally large JSON blobs.

  • Processing and compressing photos, audio and video.

  • Converting audio and video files.

  • Performing complex searching and filtering on large lists or within file systems.

  • Performing I/O, such as communicating with a database.

  • Handling a large volume of network requests.

import 'dart:isolate';

// Function running inside the isolate
void isolateFunction(SendPort sendPort) {
  // Send a message back to the main isolate
  sendPort.send('Hello from isolate!');
}

void main() {
  // Create a ReceivePort to listen for messages from the isolate
  ReceivePort receivePort = ReceivePort();

  // Spawn a new isolate and pass the sendPort of the ReceivePort
  Isolate.spawn(isolateFunction, receivePort.sendPort);

  // Listen for messages sent from the isolate
  receivePort.listen((message) {
    // Print the message received from the isolate
    print('Received message: $message');
  });
}

Performance and Isolate Group

In Dart, isolates provide a concurrency model where tasks can run independently of each other, allowing for parallel execution and improved performance. Isolate groups enhance this model by allowing multiple isolates to be managed and coordinated together as a group.

Concurrency involves executing multiple tasks simultaneously, while parallelism involves executing multiple tasks simultaneously on different processing units.

Isolate groups provide a way to manage multiple isolates collectively. Each isolate group consists of one or more isolates, which share the same root. Isolates within the same group can communicate with each other more efficiently than isolates from different groups.

import 'dart:isolate';

// Function running inside the isolate
void isolateFunction(SendPort sendPort) {
  // Send a message back to the main isolate
  sendPort.send('Hello from isolate!');
}

void main() {
  // Create a ReceivePort to listen for messages from the isolate
  ReceivePort receivePort = ReceivePort();

  // Spawn a new isolate and pass the sendPort of the ReceivePort
  Isolate.spawn(isolateFunction, receivePort.sendPort);

  // Listen for messages sent from the isolate
  receivePort.listen((message) {
    // Print the message received from the isolate
    print('Received message: $message');
  });
}

Limitations of Isolates

Each isolate in Dart has its own memory space, which incurs memory overhead.

Inter-isolate communication in Dart involves message passing between isolates, which incurs communication overhead.

Isolates in Dart do not share memory with each other.

Dart isolates provide concurrency control through mechanisms such as message passing and isolates groups.

0
Subscribe to my newsletter

Read articles from Jinali Ghoghari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jinali Ghoghari
Jinali Ghoghari