Concurrency in Flutter: Exploring the Power of Isolate

As we all know, Flutter's programming language is Dart, which is single-threaded by default. This means it executes in a single thread. However, if we are working on a large application with CPU-intensive operations, the main thread may become slow, affecting the app's performance. In this article, we'll look at how to develop large applications without sacrificing performance and how to handle CPU-intensive operations in a separate thread. We will discuss the following topics:

  1. What is concurrency?
  2. What is an isolate?
  3. How does isolate work?
  4. An example of an isolate
  5. When to use isolate?
  6. Conclusion
  7. Resources

So, without further ado, let's get started.

What is concurrency?

Concurrency refers to the process of dealing with many tasks at once. It involves organizing and executing multiple tasks throughout time, but not always at the same time.

For example, A chef who manages many dishes in the kitchen is demonstrating concurrency. Just like a chef who focuses on one dish at a time while also handling many activities such as chopping ingredients, stirring pots, and checking cooking times.

Concurrency

Image by vector4stock on Freepik

What is an isolate?

Isolates in Dart are a great way to run multiple tasks at the same time without slowing down the main thread or freezing the user interface. They are particularly useful for running tasks that require a lot of processing power.

Isolates have their own memory heap, which means they don't share memory with the main thread or other isolates. This helps to prevent common issues that can occur when multiple threads access shared memory, such as race conditions or deadlocks. By using isolates, you can make your code more efficient and avoid potential problems that can arise from concurrent programming.

How does isolate work?

Dart uses isolates to create a separate execution environment that can run parallel to the main thread. This allows for concurrent programming by enabling multiple isolates to run at the same time, each with its own memory heap, event queue, and event loop. As a result, isolates can perform independent tasks without sharing memory or state with other isolates. Here is a clear, step-by-step explanation of how isolates work in Dart:

  • Isolate Creation: In Dart, you can create a new isolate by invoking the Isolate.spawn() function. This function requires two arguments: a top-level function or a static method that serves as the entry point for the isolate, and an initial message, which is usually a SendPort object. The SendPort object is used to establish a communication channel between the newly spawned isolate and the spawning isolate.
  • Communication via Message Passing: Isolates do not share memory space, which ensures that each isolate has its own heap and prevents direct access to another isolate's data. To allow communication between isolates, Dart employs a mechanism known as message passing, which involves SendPort and ReceivePort objects. A SendPort transmits messages to another isolate, whereas a ReceivePort receives messages from another isolate. Messages are passed by value, not by reference, meaning that a copy of the data is sent, ensuring that the original data remains unaltered and secure in its own isolate.
  • Execution of Tasks: Each isolate has its own event loop and event queue. When an isolate receives a message, it's added to its event queue. The event loop then processes these messages one by one by executing the associated tasks or functions. This process continues until the event queue is empty or the isolate is terminated. This mechanism enables concurrent task performance, which boosts the efficiency of your Dart and Flutter applications.
  • Termination of Isolates: An isolate can be terminated in two ways: programmatically using the Isolate.kill() method, or automatically when it has completed all the tasks in its event queue. In some situations, a specific message can be sent to the isolate, instructing it to terminate itself.

An example of an isolate

// Importing the 'dart:isolate' package to use isolates.
import 'dart:isolate';

// This function is the entry point for the isolate. It takes a list of dynamic data as input.
void sumNumbers(List<dynamic> data) {
  // The first element of the data list is cast to a list of integers.
  List<int> numbers = data[0] as List<int>;
  // The second element of the data list is cast to a SendPort object.
  SendPort sendPort = data[1] as SendPort;
  // Initialize a variable to hold the sum of the numbers.
  int sum = 0;
  // Iterate over the list of numbers and add each number to the sum.
  for (int number in numbers) {
    sum += number;
  }
  // Send the sum back to the main isolate using the sendPort.
  sendPort.send(sum);
}

void main() async {
  // Create a ReceivePort for receiving messages from the isolate.
  ReceivePort receivePort = ReceivePort();
  // Generate a list of 1,000,000 integers starting from 0.
  List<int> numbers = List<int>.generate(1000000, (index) => index);
  // Spawn a new isolate, passing the sumNumbers function as the entry point and a list containing the numbers and the sendPort as the initial message.
  Isolate isolate = await Isolate.spawn(sumNumbers,[numbers, receivePort.sendPort]);
  // Listen for messages from the isolate.
  receivePort.listen((message) {
    // Print the sum received from the isolate.
    print('Sum: $message');
    // Close the ReceivePort after receiving the sum.
    receivePort.close();
    // Kill the isolate after its task is done.
    isolate.kill();
  });
}

In the following code, a new isolate is created to calculate the sum of a list of 1,000,000 numbers. The sumNumbers function, which is the entry point for the isolate, receives the list of numbers and a SendPort for communication with the main isolate. It calculates the sum and sends it back to the main isolate. The main isolate listens for the sum, prints it, and then terminates the isolate and closes the ReceivePort.

When to use isolate?

Isolates in Flutter are especially useful when you need to perform big computations or I/O operations without blocking the UI or producing junk. Here are some real-world scenarios where you might use isolates:

  • Image Processing: If your Flutter application involves heavy image processing tasks, such as filtering, resizing, or format conversion, you can perform these operations to an isolate.
  • Data Processing: If your application requires processing large amounts of data, such as parsing a large JSON response from an API, it can be beneficial to perform this operation in an isolate.
  • Complex Calculations: If your application involves complex calculations such as machine learning algorithms, statistical computations, or cryptography, it is recommended to perform these operations in an isolate.
  • File Operations: If your application needs to read or write large files, it can be beneficial to perform these operations in an isolate.

It is critical to remember that the purpose of using isolates is to keep the UI thread free so that it can respond to user input and make screen updates. An isolate is appropriate for any task that has the potential to freeze the UI thread for an extended period of time. Delegating these tasks to isolation ensures that your application remains responsive and provides a seamless user experience.

Conclusion

In this article, we discussed concurrency and how isolates can handle heavy computation tasks in a separate thread to keep your app running smoothly. We also provided an example of when it's appropriate to use isolates. Ultimately, if you're performing CPU-intensive tasks in your app that are slowing down performance, it's worth considering using isolates. Thank you for reading this article. If you found it helpful, feel free to connect with me on LinkedIn and say hi.

Resources


Similar Articles