Understanding Threads and Isolates in Flutter
Concurrency and parallelism are essential for building responsive and performant Flutter applications. In this article, we’ll take a closer look at threads and isolates, two key concepts for enabling concurrency and parallelism in Flutter.
Understanding Threads
A thread is a sequence of instructions executed concurrently with other threads. Threads allow an application to perform multiple tasks simultaneously, which can be particularly useful for time-consuming operations such as network requests or file input/output.
In Flutter, the main thread is the UI thread, responsible for rendering the user interface and processing user input. However, performing long-running operations on the UI thread can cause the application to become unresponsive, leading to a poor user experience. To avoid this, developers can use background threads to perform such operations.
Flutter provides two types of threads: the UI thread and the background thread. The UI thread is responsible for rendering the user interface and processing user input. In contrast, the background thread is used for time-consuming operations such as network requests or file input/output.
Understanding Isolates
An isolate is a standalone instance of the Dart runtime that can run in parallel with other isolates. Each isolate has its own memory heap, which means that isolates do not share the memory with each other. This allows isolates to perform operations in parallel without worrying about concurrency issues such as race conditions.
In Flutter, isolates are used to perform background tasks that require a high degree of parallelism, such as image processing or audio decoding. Isolates are created using the Isolate.spawn()
function, which creates a new isolate and passes it a callback function to execute.
Isolates in Dart have a few important characteristics:
- They are similar to threads, in that they represent a separate flow of execution within the application.
- Isolate memory is not shared between isolates, which means that each isolate has its own heap and call stack.
- Isolates communicate with each other using ports and messages. Each isolate has its own set of ports, which can be used to send and receive messages to and from other isolates.
- Isolates can be used to take advantage of multiple processor cores, if available. Because each isolate runs in its own memory space, multiple isolates can execute in parallel without interfering with each other.
- Isolates can be used to perform work in parallel, which can improve the performance of the application by reducing the amount of time spent waiting for blocking operations to complete.
In Flutter there are four runners that handle different aspects of the application:
- UI Runner: The UI Runner handles the rendering and layout of the application’s user interface. It runs on the main UI thread and is responsible for updating the UI in response to user input or data changes.
- GPU Runner: The GPU Runner handles graphics rendering using the device’s GPU. It runs on a separate thread and communicates with the UI Runner to update the UI with new graphics.
- IO Runner: The IO Runner is responsible for handling input and output operations, such as network requests or file I/O. It also runs on a separate thread and communicates with the UI Runner to update the UI with new data.
- Platform Runner: The Platform Runner is responsible for handling platform-specific functionality, such as accessing native device features or integrating with other native applications. It runs on a separate thread and communicates with the other runners as needed to perform platform-specific tasks.
It’s worth noting that although each runner is responsible for different aspects of the application, they all share the same underlying platform runner. This allows them to communicate and coordinate with each other as needed to provide a smooth and responsive user experience.
In Flutter applications, there are often multiple sources of work that need to be processed, such as user input, timers, and network requests. These tasks cannot all be executed in a single step-by-step line of code, so Flutter uses an event loop to manage the processing of these tasks.
The event loop in Flutter works by maintaining a queue of events that need to be processed. These events can come from a variety of sources, such as user input or timer callbacks. When an event is received, it is added to the end of the event queue.
The event loop then begins processing events from the front of the queue, one at a time. When an event is processed, it is removed from the front of the queue, and any associated work is executed.
In addition to the event queue, Flutter also uses a microtask queue to manage the execution of asynchronous code. Microtasks are small units of work that need to be executed asynchronously but are not triggered by an external event. Instead, they are typically used for processing work that needs to be executed as soon as possible, such as updating the user interface after some state has changed.
When a microtask is added to the microtask queue, it is executed immediately after the current event has been processed, but before any new events are processed. This means that microtasks have higher priority than regular events, and can be used to ensure that important work is executed as soon as possible.
Implementation
Isolate
you can create isolated processes using the Isolate
class. An isolate is a separate instance of the Dart virtual machine that runs concurrently with the main isolate. Each isolate has its own memory heap and runs independently of the other isolates. This can be useful for running long-running or computationally intensive tasks without blocking the main UI thread.
Here is an example of how to create an isolate in Flutter:
import 'dart:isolate';
void runIsolated(SendPort sendPort) {
// Do some computation or other long-running task here
for (int i = 0; i < 10; i++) {
print(i);
}
sendPort.send('Task completed');
}
Future<void> runIsolate() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(runIsolated, receivePort.sendPort);
receivePort.listen((message) {
print('Received message: $message');
setState(() {
text = message;
});
});
}
In this example, we first create a ReceivePort
, which allows us to listen for messages sent from the isolate. We then create a new isolate using the Isolate.spawn()
method, passing in the function runIsolated
as the entry point for the isolate. The sendPort
parameter of runIsolated
is a SendPort
the object that allows us to send messages back to the main isolate.
In the runIsolated
function, we perform our long-running task and then send a message back to the main isolate using the sendPort
object.
Finally, we listen for messages sent from the isolate using the receivePort.listen()
method, and print out any messages received.
Note that you can also pass arguments to the Isolate.spawn()
method if you need to initialize the isolate with some data. You can also create multiple isolates to run different tasks concurrently.
Compute()
you can also use the compute()
function to run computationally intensive tasks in a separate isolate. This function is a simpler way to run isolates compared to using the Isolate
class directly, as it handles the creation of the isolate and passing of arguments for you.
Here is an example of how to use the compute()
function:
String reverseString(String input) {
return input.split('').reversed.join();
}
Future<void> runCompute() async {
String originalString = 'Hello world';
String reversedString = await compute(reverseString, originalString);
print(reversedString);
setState(() {
text = reversedString;
});
}
In this example, we define a function reverseString
that takes a string as input and returns the string in reverse order.
We then call the compute()
function, passing in the reverseString
function as the first argument and the original string as the second argument. The compute()
function will automatically create an isolate and run the reverseString
function in that isolate with the provided arguments.
The result of the computation is returned as a Future
so we use the await
keyword to wait for the result to be available. We then print out the reversed string.
can communicate between two isolated threads using asynchronous message passing.
What are ReceivePort
and SendPort
?
ReceivePort
and SendPort
are two classes that are used for inter-isolate communication in Flutter.
- A
SendPort
is used to send messages from one isolate to another. - A
ReceivePort
is used to receive messages from other isolates.
SendPort
and ReceivePort
work together as a pair. A SendPort
is used to send messages to a specific ReceivePort
and a ReceivePort
is used to receive messages from a specific SendPort
.
Using ReceivePort
and SendPort
To use ReceivePort
and SendPort
, you need to create instances of both classes in your isolates.
Creating a ReceivePort
To create a ReceivePort
, you simply need to create a new instance of the ReceivePort
class. Here's an example:
ReceivePort receivePort = ReceivePort();
In this example, receivePort
is an instance of ReceivePort
that you can use to receive messages from other isolates.
Sending Messages
To send a message from one isolate to another, you need to get a SendPort
for the receiving isolate and use it to send the message. Here's an example:
// get a SendPort for the receiving isolate
sendPort.send("Hello, world!");
In this example, we’re sending a string message to the receiving isolate.
Receiving Messages
To receive messages from other isolates, you need to listen to the ReceivePort
. Here's an example:
ReceivePort receivePort = ReceivePort();
receivePort.listen((message) {
print("Received message: $message");
});
In this example, we’re listening to receivePort
and printing any messages that we receive.
Closing a ReceivePort
Once you’re done using a ReceivePort
, you should close it to free up any resources that it's using. Here's an example:
receivePort.close();
So that, ReceivePort
and SendPort
are powerful tools for inter-isolate communication in Flutter. With these classes, you can easily send and receive messages between isolates in your app. Just remember to close your ReceivePort
s when you're done using them to avoid leaking resources.
example
Here’s an example of how you can use ReceivePort
and SendPort
to send and receive messages between two isolates in Flutter:
import 'dart:isolate';
class Message {
final String content;
Message(this.content);
}
void runIsolateFunctionCommunication() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(isolateCommunicationFunction, receivePort.sendPort);
receivePort.listen((message) {
if (message is SendPort) {
SendPort sendPort = message;
sendPort.send(Message("Hello from the main isolate!"));
} else if (message is Message) {
print("Received message: ${message.content}");
}
});
}
void isolateFunctionCommunication(SendPort sendPort) {
// Create a receive port to listen for incoming messages
ReceivePort receivePort = ReceivePort();
// Send the send port of the receive port to the main isolate
sendPort.send(receivePort.sendPort);
// Wait for a message from the main isolate
receivePort.listen((message) {
print("Received message from main isolate: $message");
// Send a message back to the main isolate
sendPort.send("Hello from isolate!");
});
}
In this example, we have two functions: runIsolateFuntionCommunication()
and isolateFuntionCommunication()
sets up a ReceivePort
and spawns an isolate using Isolate.spawn()
. The SendPort
for the new isolate to be sent to isolateFuntionCommunication()
through the ReceivePort
.
The runIsolateFuntionCommunication()
the function then listens for messages on the ReceivePort
. When it receives SendPort
the new isolate, it sends an Message
object to it. When it receives an Message
object from the isolate, it prints the content of the message.
The isolateFuntionCommunication()
sets up its own ReceivePort
and listens for messages. When it receives an Message
object, it prints the content of the message and sends a new Message
object back to the main isolate using the SendPort
.
Note that isolates are independent of each other and run in separate memory spaces. Any data that you want to pass between isolates need to be serialized and deserialized. In this example, we’re using a simple Message
class to pass data between the two isolates.
Stopping Thread
how to stop isolating?
In Dart, you cannot directly kill a thread, but you can request that a running isolate (which is the Dart equivalent of a thread) be stopped by using Isolate.kill()
.
Here’s an example of how to use Isolate.kill()
to stop a running isolate:
import 'dart:isolate';
void runInfiniteInIsolate(int seconds) {
print("Isolate running");
// Do some work here...
Timer.periodic(Duration(seconds: seconds), (timer) {
print(timer.tick);
});
}
stopIsolate() async {
Isolate isolate = await Isolate.spawn(runInfiniteInIsolate, 1);
// Wait for 5 seconds and then kill the isolate
await Future.delayed(const Duration(seconds: 3));
isolate.kill(priority: Isolate.immediate);
print("Isolate stopped");
}
In this example, Isolate.spawn()
is used to create a new isolate that runs the stopIsolate()
function. The runInfiniteInIsolate()
function waits for 5 seconds and then kills the isolate using isolate.kill()
. The priority
the parameter determines the priority of the kill request. In this example, we're using, Isolate.immediate
which means that the isolate will be stopped as soon as possible.
Note that Isolate.kill()
only requests that the isolate be stopped. It does not guarantee that the isolate will be stopped immediately or at all. The isolate may continue to run until it completes its current work. Therefore, it's important to design your isolate so that it can be safely stopped at any point in its execution.
How to stop compute()?
It is not possible to kill a compute
function.
Why
The reason for this is that looking at the source code of the compute
function, the created isolate
is only killed after the result
completer has finished, The result is only completed if there is either an error or the function you pass to compute
returns.
Solution
If you want to be able to kill the isolate you launch, do not use compute
. Instead, you will have to create them Isolate
yourself.
In this tutorial, we’ve learned that threads and isolates are powerful tools that allow Flutter developers to manage the concurrency of their applications. By using threads and isolates, developers can create high-performance applications that can perform multiple tasks at the same time without sacrificing the user experience.
I hope you all liked this blog and it helped you start with Flutter! Don’t forget to smash that clap button and leave a comment down below.
If you liked this article make sure to 👏 it below, and connect with me on Portfolio, Github, and LinkedIn.
Meet you at the next one.