Single Thread Dart, What? — Part 2

Dave Parth
Globant
Published in
5 min readJan 7, 2020

--

If you have not read the first part of this series then read here before continuing.

An Isolate is a Thread. Isolates do not share memory as we have covered the main isolate have their event loop, it’s the same for other isolates also and so to send and receive processed data you need to use Ports to communicate back-and-forth.

“Isolates” in Flutter do not share memory. Interaction between different “Isolates” is made via “messages” in terms of communication.

You can create isolate 2 ways:

  1. Use compute function and it will handle all communication process for data sending and receiving.
  2. Write all logic to send and receive data back-and-forth your self.

Isolates are kind of TCP Socket. We need to create a handshake mechanism for this. There is also another reason to do this is, as we define earlier Isolate does not share memory and you need to use ports and messages to pass data back-and-forth. That is why we need to use some kind of handshake.

There are some rules for creating Isolate

  1. Isolates need to have a non-static top-level function.
  2. If you use compute a function or Isolate.spawn function for now function must have one parameter. You may ask why? check the method signature.
external static Future<Isolate> spawn<T>(
void entryPoint(T message), T message,
{bool paused: false,
bool errorsAreFatal,
SendPort onExit,
SendPort onError,
@Since("2.3") String debugName});

Check entryPoint(T message) method call, It takes T message and so you are required to have a single parameter function.

3. Isolates take up around ~2MB per Isolate and it's lightweight so use it carefully. Isolate memory size and allocation - view for more details

4. Every message pass from Isolates requires message size * 2 memory as it will be two copies on system since Isolate do not share memory, So if you have 1 GB of file and want to pass from one Isolate to second it will take 2Gb of memory on device (on device I mean RAM - check reference links for this details).

5. Isolates and Ports can be alive even though you have completed your work or all statements are executed from the method. So it's up to us to kill isolates and close the ports.

Code(No Flutter code for this pure Dart code):

import 'dart:isolate';void main() async{
startIsolate();
}

imported Isolate package and called startIsolate function. We will start by creating ReceivePort which is used to convey a message back and forth. The thing to remember ReceivePort Stream can only be used one time, so if we send data one time and taking data from receivePort.first now when we send data again and try to get data from receivePort.first it will not work and will throw an error with the statement: Unhandled Exception: Bad state: Stream has already been listened to. The most common form of Stream can listen only once at a time. If you try to add multiple listeners it will throw an exception. so we will use receivePort.first to get SendPort back and forth and if we want to use just one ReceivePort for better optimization, we need to use listen to methods from that class. Steps to the handshake (Will give dummy examples to have a clear idea of what classes are used for)

  1. Create Receive Port object (example: Receive Port is kind of like exposed server)
  2. Spawn Isolate with the send port (example: Send port is kind of like a port like a socket 8080 port where we launch our socket and others can access it by calling website…:8080 and server will give the response to that request on that port)
  3. In Isolate we need to use a handshake, What I mean by that is We have 2 threads and we need to communicate messages so we need to have some stream where we listen for both sides of events and so we will pass sendPorts first to establish where we need to send the message.
  4. So after creating Isolate by spawn, we will wait for Isolate to send its SendPort, to which we will ping for msg.
  5. At the side of Isolate, we will again create ReceiverPort and send a message to the SendPort we have passed in method argument, and the message would be the SendPort of this Isolate.
  6. Remember to use receiverPort.first only one time and we will get the Send Port.
  7. Now once we get all ports, It’s like Socket open channel we will send messages back and forth.
startIsolate() async{
//create ReceivePort
ReceivePort receivePort = ReceivePort();
//receivePort.sendPort is our destination for receiving message
var isolate = await Isolate.spawn(calculateFunction, receivePort.sendPort);
SendPort sendPort = await receivePort.first;
// we got Isolate's SendPort or should I say Message destination
}
calculateFunction(SendPort sendPort) async{
var calculateFunctionReceivePort = ReceivePort();
sendPort.send(calculateFunctionReceivePort.sendPort);
await for (var msg in calculateFunctionReceivePort){
var data = msg[0];
SendPort replyTo = msg[1];
replyTo.send("$data from Isolate");
print("Received inside Isolate: $data");
}
}

Once we get the sendPort of the Isolate we will send a message and listen for isolate response.

import 'dart:isolate';void main() async{
startIsolate();
}
startIsolate() async{
ReceivePort receivePort = ReceivePort();
var isolate = await Isolate.spawn(calculateFunction, receivePort.sendPort);
SendPort sendPort = await receivePort.first;
receivePort.close(); // closing the port , do not forget to close it.
var msg = await sendStreamData(sendPort, "Test Data");
print(msg);
Future.delayed(Duration(seconds: 1),()async{
msg = await sendStreamData(sendPort, "Test Data 2");
print(msg);
isolate.kill(priority: Isolate.immediate);
});
}
sendStreamData(SendPort sendPort, String msg) async{
ReceivePort sendStreamDataPort = ReceivePort();
sendPort.send([msg, sendStreamDataPort.sendPort]);
var response = await sendStreamDataPort.first;
sendStreamDataPort.close();
return response;
}
calculateFunction(SendPort sendPort) async{
var calculateFunctionReceivePort = ReceivePort();
sendPort.send(calculateFunctionReceivePort.sendPort);
await for (var msg in calculateFunctionReceivePort){
var data = msg[0];
SendPort replyTo = msg[1];
replyTo.send("$data from Isolate");
print("Received inside Isolate: $data");
}
}

output:

Received inside Isolate: Test Data
Test Data from Isolate
Received inside Isolate: Test Data 2
Test Data 2 from Isolate

Questions:

  1. Why we use ReceiverPort? or What is ReceiverPort in Isolate?
  2. Why do we need to use Isolate when we have async code?
  3. How long Isolates live? and how to kill or close it?
  4. Does Isolates Share memory? If no then how to pass data?

Reference Links:

  1. Isolates benchmarks profilers example and more depth
  2. Flutter rendering pipeline
  3. Flutter Isolates and Event-loop explanation
  4. Isolate memory size and allocation
  5. Isolate and event loop intro video from Flutter Devs
  6. Isolate memory size and allocation

This concludes the Flutter Single-Threaded Dart series. I hope you liked it.

Do you know you can press the clap👏 button 50 times? The higher you go, the more it motivates me to write more stuff for you guys!

Hello, I am Parth Dave. A noob developer and noob photographer. You can find me on Linkedin or stalk me on GitHub or maybe follow me on Twitter?

Have a nice fluttery day!

--

--

Dave Parth
Globant

Developer, Photographer. Flutter and Automation enthusiast.