Decoding Isolates: Basic to Advanced Concepts — Part1

James Cardona Orozco
7 min readFeb 20, 2024

--

This serie will provide a comprehensive exploration of Dart isolates, covering fundamental concepts and advanced techniques. It will delve into the basics of isolates, their role in concurrency, and how they relate to futures and streams. The discussion will extend to advanced topics, such as message passing, Isolates communication, and optimizing performance also I’ll show two practical demos.

This post is the first part of three;

  • First part, we are going to see the theory in deep to understand Isolates.
  • The second part introduces a way to create an Isolate Wrapper/Controller
  • The third part is demo time around Isolates.

What are isolates?

An Isolate in Dart enables concurrent processing, allowing multiple tasks to run simultaneously. It operates as a “thread” with its event loop and separate memory space (it’s a thread wrapper).

One of the differences with threads is Isolates don’t share memory and communicate with each other through messages using the Actor Model (We’ll visit this later).

In Flutter, they perform intensive tasks in a separate space other than the main one, preventing delays in the user interface.

  • Isolates enable Dart code execution separate from the main thread.
  • They facilitate concurrent operations without blocking the UI.
  • Isolates operate as distinct Dart VM instances enabling parallelism processing.
  • Each isolate has its own memory space, state, and event loop.

Why do we need them?

Before we get into this, we first need to understand how async-await and the event handling.

Event Loop

Dart is single-threaded, which means it can only execute one task at a time. This is where the event loop comes in. The event loop is a queue of tasks that are executed in order.

Imagine you’re in a busy coffee shop where a barista handles orders efficiently. The barista represents Flutter’s event loop, constantly processing orders (events) from customers (user interactions). Just like the barista ensures everyone gets served promptly, Flutter’s event loop keeps the app responsive to user actions and updates.

Async-Await

Async doesn’t mean parallel, it means non-blocking. When you call an async function, it returns a Future immediately. The function continues to execute, and when it’s done, it completes the Future. This is the basis of the event loop, which is a queue of tasks that are executed in order.

Suppose you have an I/O operation to read a JSON file, the event loop will work like this:

1- We receive the request to read the file.
2- The event loop will add the task to the queue.
3- The event loop will continue to process other tasks.
4- When the file is read, the event loop will execute the callback.

Future with the event loop

More information about this can be found in the Dart Futures — Flutter in Focus

Event handling

Isolate encapsulates a wrapper around thread; each isolate ha with its event loop managing queued events. Events, representing user interactions, are processed by the event loop in a FIFO manner. This ensures the timely execution of app actions.

Event handling

For a moment let’s imagine that we have to do an I/O operation like reading a JSON; that this file is very large and the task takes a long time to complete. Now the event queue and the event loop look similar to the image below.

Jank

Since the main isolate struggles to promptly process events, our animations or UI may freeze, frustrating users and causing significant drop-offs. Here’s where creating a new isolate or a worker isolate becomes crucial.

Let’s get back to Isolates.; why do we need them?

1. Concurrency: Execute Dart code simultaneously, without affecting the main thread.
2. Isolation: Each isolate has its memory, avoiding data issues.
3. Parallelism: Run code on multiple CPU cores for better performance.
4. Communication: Isolates exchange messages to share information.
5. Heavy task: Like image processing or data fetching.

How to implement Isolates?

Basically, there are two ways to create an isolate:

- Compute Isolate (This uses Isolate.run)
- Spawn Isolate (This uses Isolate.spawn)

As we mentioned before, Isolates unlike threads, don’t share memory, and communicate with each other through messages using the Actor Model.
This means that you can’t share data between isolates, you need to send messages to communicate between them.

The entrypoint of an isolate is a function and must be a top-level or static function. Depending on the method you use to create an isolate, you can pass arguments to the function.

Here is the link to the code if you want to follow along.

Imagine you need to calculate a progressive sum of a number.

void doSomething(var bigNumber) {
print('Doing something');

var sum = 0;
for (var i = 0; i <= bigNumber; i++) {
sum += i;
}
print('finished ${sum}');
}

If you try to execute this function without using an isolate, you will notice that the UI will freeze for a few seconds. This is because the main isolate is busy calculating the sum, and it can’t process any other events.

Compute Isolate

The `compute` or `Isolate.run` function creates an isolate and runs the specified function in it. This is useful for short-lived tasks that don’t require continuous communication between isolates.

You can use `compute` or `Isolate.run` to execute the function in a separate isolate. Both methods return a Future that completes with the result of the function.

void computeIsolate() {
print('Compute');
compute(doSomething, 1000000000);
}

void runIsolate() {
print('Run');
Isolate.run(() => doSomething(1000000000));
}

Spawn Isolate

The `spawn` function creates an isolate and runs the specified function in it. This is useful for long-lived tasks that require continuous communication between isolates.

void spawn() async {
print('Spawn');
final rcvPort = ReceivePort();

final isolate = await Isolate.spawn(_doSomethingForSpawn, rcvPort.sendPort);

final completer = Completer<SendPort>();
rcvPort.listen((message) {
if (message is SendPort) completer.complete(message);

print(message);

if (message is! SendPort) {
rcvPort.close();
isolate.kill();
}
});

final send2Isolate = await completer.future;
send2Isolate.send(1000000000);
}

In the next image, you can see the difference between doing the execution in the main isolate and using an isolate.

The code of this specific example is here link; inside the “learning 1 folder”.

I’ll return later to explain the steps of the spawn function also we need to cover what is a SendPort and a ReceivePort, and how to use them to communicate between isolates; before that, I want to give you a brief explanation of the Actor Model.

Actor Model

If you want to understand in more detail what the paradigm is that drives how isolates work?, this section is a summary of it; As a personal appreciation, this helped me understand how to create good architecture around the Isolates; but you are free to skip to the next session.

Concurrency models

  • Processes
  • Threads
  • Futures
  • Coroutines
  • Actor
  • etc

What is the Actor Model?

The actor model is a concurrent programming that is based on the concept of actors. An actor is an entity that encapsulates state and behavior, communicates with other actors by sending and receiving messages, and processes messages sequentially.

Key concepts:

- Actors are persistent.
- Encapsulate internal state (Private).
- Actors are asynchronous.
- Communication through messages.
- Independence between actors.
- Supervision.

What can actors do?
- Create new actors.
- Send messages to other actors.
- Receive messages and in-responses.
- Process exactly one message at a time.

“Do not communicate by sharing memory; instead, share memory by communicating” | Effective Go

Properties of communication

  • NO channels or intermediaries.
  • “best effort” delivery.
  • Messages can take an arbitrarily long time to deliver.
  • No message ordering guarantees.

Address

  • Each actor has an address.
  • Actors can communicate with other actors using their addresses (Use SendPort to send messages).
  • The actor receives addresses from other actors in messages (Listen ReceivePort).
  • One actor can have more than one address.
  • Address != identify; this means two actors with the same identity can have different addresses.

Supervision

The running state of an actor is monitored and managed by another
- Constantly monitors the running state of the actor (is alive, restart)
- Can perform actions based on the state of the actor (eg unhandled error)

Supervisor scheme

Isolates similarities

  • ReceiverPort is a similar concept to Actor Mailbox.
  • The Mailbox is a message queue.
  • SendPort is a similar address concept in the actor.

This is the basic concept of the Actor Model, and it’s the base of how isolates work. With this in mind, we can understand how to use `SendPort` and `ReceivePort` to communicate between isolates.

Here is a summary of the difference between Isolates and Threads that are provided by the Actor Model.

  • Isolation of memory: Isolates don’t share memory and the data is passed through messages.
  • Lightweight: You can create thousands of isolates.
  • Safe concurrency: Isolates provide a safe way to perform concurrent operations, minimizing the risk of common concurrency issues like deadlocks.

--

--

James Cardona Orozco

Task-driven Software Engineer with 10+ years of experience developing mobile, backend, and web3 applications. Anime, Poker & Books lover