As developers, we face the challenge of writing programs that perform multiple independent tasks simultaneously. The introduction of concurrency in the Dart language provides an effective solution to this problem. These concurrent operations can execute in the main isolate or in a newly spawned isolate, depending upon the requirement of the code. Dart concurrency allows different parts of the code to be executed simultaneously, making the Flutter apps more responsive and efficient.
The main isolate is where the Dart code runs initially when a Flutter application is launched. Isolates in Flutter, based on the Dart isolates concept, run each separate Dart code in a new isolate, making it a unique Dart execution context. As a Dart developer, while dealing with Dart concurrency, it is critical to understand various terms like an event loop, main isolate, spawned isolate, event queue, and more.
One unique aspect of the Dart platform is that each isolate has its own event loop and memory, which allows the code to run independently. This mean that each isolate has its own memory heap which can't be accessed by any other isolates. It can only communicate with other isolates using message passing mechanism.
In the upcoming sections, we will explore more about the event loop, new isolates, how to perform multiple independent tasks using isolates, and a practical Flutter isolate example.
Asynchronous programming plays a vital role in writing concurrent code. Employing asynchronous code allows our programs to accomplish more by doing multiple tasks at the same time.
The concept of an event loop is integral to running asynchronous code in Dart. The event loop acts as a container for tasks (messages) that are queued to be processed. A Dart program runs in the context of an event loop, managing the execution of these tasks. The main isolate in a Dart program has its own event loop where tasks are processed.
Whenever an isolate is created, it's initialized with its own event loop. Each event in the event loop is processed one at a time—in the order they're received and added to the event loop. When the event loop's task queue is empty, the Dart program is done and quits.
One crucial thing to remember is that each Dart isolate has its 'own event loop', providing a completely isolated Dart execution context. This makes the main isolate and spawned isolates independent of each other.
Asynchronous operations are a critical piece in Dart programs. Dart's core libraries give programmers a host of powerful, flexible asynchronous APIs to work with.
Working with Dart concurrency demands a solid understanding of Dart isolates. Dart Isolate, a word derived from “isolation,” is a part of code that runs independently of other parts. Each isolate has its own memory space, meaning variables from one isolate aren’t accessible from a different isolate.
This is where the Dart isolate model deviates slightly from conventional threads. While threads in the same process share memory, isolates don't. As a result, each Dart isolate leverages its own memory, ensuring the Dart code runs in an isolated environment. This reduces the potential for common threading problems like race conditions.
A Dart program starts executing code in the "main isolate," created by the Dart runtime. When you spawn a new isolate, the system creates a new event loop for that isolate where you can run code parallel to your main isolate.
However, remember that executing code in a new isolate does not necessarily mean it happens concurrently. The Dart Virtual Machine (DVM) can run multiple isolate event loops concurrently, but it doesn't guarantee that it will do so. The operating system decides whether to provide a new thread for your new isolate’s event loop.
Core to the communication between the main isolate and the spawned isolate are "control port" and "message passing". Isolate holds the control port of the worker isolate—the new isolate—to which it can send messages. On the other hand, the worker isolate needs to know the control port of the main isolate to send messages back. This mechanism ensures that each isolate operates in its own memory space, sharing no state with the rest of the system.
In most other languages, thread-based concurrency is used where threads share the same memory space. But Dart uses isolates for concurrent programming. In the Dart isolate model, each isolate has its own memory and runs individual Dart code. Isolates communicate by sending messages over channels. It's important to note that isolates run code independently without any shared state, offering granular control over execution.
To create isolates, Dart provides a spawn function in the Isolate class. For example, the spawn function is used to start a new isolate that executes the function named ‘foo’.
1 Isolate.spawn(foo, message); 2
The newly spawned isolate starts executing the 'foo' function.
Since each isolate has its own memory, different isolates cannot work with shared variables. However, isolates can interact by sending messages to each other. Certain tasks, like fetching data from a server or a database, may take a long time to complete. While that task is working in a separate isolate, the main isolate can continue executing other tasks. When the long-running task completes, the spawned isolate can use a callback to notify the main isolate of this event.
In the code block below, an isolate sends multiples messages to the main isolate, and once all messages are received, it signals that it's done by closing the receive port.
1 Future<void> main() async { 2 ReceivePort port = ReceivePort(); 3 4 await Isolate.spawn(echo, port.sendPort); 5 6 await for (var msg in port) { 7 print(msg); 8 if (msg[0] == "DONE!") { 9 port.close(); 10 } 11 } 12 } 13 14 echo(SendPort sendPort) { 15 sendPort.send(["Hello", "there"]); 16 sendPort.send(["Welcome", "to Dart!"]); 17 sendPort.send(["DONE!"]); 18 } 19
Isolates in Dart, thus usher in a new approach to concurrent programming.
With an understanding of how Dart manages concurrent programming, let's proceed to apply the same to Flutter. Flutter employs the concept of Dart isolates, introducing the Dart concurrency model to Flutter app development, which enhances the performance and responsiveness of your applications.
In Flutter, the main UI thread also runs on an isolate commonly called the "main isolate." All Flutter UI related tasks run on this main isolate, and whenever we create new isolates (worker isolates) to perform non-UI tasks, they run parallel to the main isolate.
It's imperative to note that since each isolate has its "own memory," data cannot be shared between different isolates. However, they can communicate by sending and receiving messages asynchronously. Here's a simple example of communication between two isolates:
1 void main() { 2 Isolate.spawn(isolateTest, 'Hello from main isolate'); 3 } 4 5 void isolateTest(String message) { 6 print(message); 7 } 8
In this example, we spawn a second isolate (worker isolate) that runs the 'isolateTest' function and pass a message from the main isolate. It illustrates the basic principle of isolates relaying messages via message ports.
Ports are key to understanding how isolates communicate with each other. In essence, a port serves as an entry point to receive messages in an isolate. Each isolate has its control port to manage its message queue.
When we spawn a new isolate, we create a communication channel (SendPort and ReceivePort) for the new isolate and the current isolate to communicate with each other using message passing. Isolates run code and send data back to the main isolate upon completion.
The following code snippet shows how a new isolate shares the result with the main isolate using control port:
1 Future<void> main() async { 2 ReceivePort receivePort = ReceivePort(); 3 Isolate.spawn(echoReceivePort, receivePort.sendPort); 4 5 SendPort childSendPort = await receivePort.first; 6 7 List msg = await sendReceive(childSendPort, "Hello from Main Isolate"); 8 9 print(msg); 10 } 11 12 echoReceivePort(SendPort sendPort) { 13 ReceivePort port = ReceivePort(); 14 15 port.listen((msg) { 16 String data = msg[0]; 17 SendPort replyPort = msg[1]; 18 replyPort.send([data, port.sendPort]); 19 }); 20 21 sendPort.send(port.sendPort); 22 } 23 24 Future<List> sendReceive(SendPort port, msg) { 25 ReceivePort response = ReceivePort(); 26 port.send([msg, response.sendPort]); 27 return response.first; 28 } 29
This example starts with the main isolate creating a ReceivePort and a new isolate with the SendPort passed as an argument. The new isolate attaches a ReceivePort and sends the ReceivePort‘s SendPort back to the main Isolate, establishing a two-way communication pipeline.
After gaining a robust understanding of isolates and how they enable concurrency, it's time to add some practical implementation of isolates in a Flutter application.
Before jumping into the code, it's essential to ensure that your Flutter development environment is correctly set up. This comprises Flutter SDK, a programming IDE with Flutter plugins, and a functional emulator or a physical device for testing.
Planning beforehand can pay off when working with complex functionalities like isolates. In this example, our main components are: • The 'main isolate' that runs the UI. • A 'worker isolate' that performs the concurrent task. • The 'message passing' mechanism for communication between the main and worker isolate.
Let's start with creating our worker isolate. Here we implement a simple computation example: calculating the nth Fibonacci number:
1 topLevelFunction(var message) { 2 int computeFib(int n) { 3 return n < 2 ? n : (computeFib(n - 1) + computeFib(n - 2)); 4 } 5 print('Fibonacci ${computeFib(message)} computed in worker Isolate'); 6 } 7
Next, we use the Isolate.spawn() method to spawn a worker isolate and have it execute the top-level function:
1 void main() { 2 Isolate.spawn(topLevelFunction, 30); 3 print('Executing from the Main Isolate'); 4 } 5
When we run the above code, we will see that the execution from the worker isolate and the main isolate are both carried out simultaneously, showcasing Dart concurrency with isolates.
Adding a worker isolate in our Flutter app made quite a difference. The main isolate doesn't get blocked while the worker isolate was busy calculating, and the application was responsive during this time. This is a great illustration of how Flutter and Dart isolates can help improve your app's performance by leveraging the capacity of modern multicore processors.
It's crucial to understand that while Dart isolates provide a robust foundation for concurrent programming, using them effectively can be a challenge. Here, we'll discuss some of the potential pitfalls and best practices when dealing with isolates.
Notice: Isolates are a helpful tool to have in your toolbox, but they're not always the best solution for every problem. Threads or multiple processes might be better solutions, depending on the specific requirements of your application or the resources available on your machine.
We've now explored the fascinating world of Dart concurrency using isolates, from the theoretical concepts to their practical application in Flutter. With the understanding of isolates and the ability to utilize them effectively, you can ensure the concurrent execution of Dart code to build performant and user-friendly Flutter applications. Just remember to navigate the common pitfalls and stick to recommended best practices, and you'll be able to fully utilize the power of Flutter isolates.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.