Using isolates in the Dart VM

Dart code runs in a single thread, in what is called a single process or isolate. In the standalone Dart VM, all code starts from main() and is executed in the so-called root isolate. To make an app more responsive or to increase its performance, you can assign parts of the code to other isolates. Moreover, because there are no shared resources between isolates, isolating third-party code increases the application's overall security. A Dart server app or command-line app can run part of its code concurrently by creating multiple isolates. If the app runs on a machine with more than one core or processor, it means these isolates can run truly in parallel. When the root isolate terminates the Dart VM exits and, with it, all isolates that are still running. Dart web apps currently can't create additional isolates, but they can create workers by adding instances of the dart:html Worker class, thereby adding JavaScript Web workers to the web app. In this recipe, we show you how to start up a new isolate from the main isolate and how to communicate with it using ports.

How to do it...

Examine the code in using_spawn.dart to create isolates with spawn, as shown in the following code:

import'dart:async';
import'dart:isolate';

main() {
  // 1- make a ReceivePort for the main isolate:
  varrecv = new ReceivePort();
  // 2- spawn a new isolate that runs the code from the echo      // function
  // and pass it a sendPort to send messages to the main isolate
  Future<Isolate> remote = Isolate.spawn(echo, recv.sendPort);
  // 3- when the isolate is spawned (then), take the first message
  remote.then((_) =>recv.first).then((sendPort) {
  // 4- send a message to the isolate:
  sendReceive(sendPort, "Do you hear me?").then((msg) {
  // 5- listen and print the answer from the isolate
  print("MAIN: received $msg");
  // 6- send signal to end isolate:
  returnsendReceive(sendPort, "END");
  }).catchError((e) => print('Error in spawning isolate $e'));
  });
  }
  
  // the spawned isolate:
  void echo(sender) {
  // 7- make a ReceivePort for the 2nd isolate:
  var port = new ReceivePort();
  // 8- send its sendPort to main isolate:
  sender.send(port.sendPort);
  // 9- listen to messages
  port.listen((msg) {
  var data = msg[0];
  print("ISOL: received $msg");
  SendPortreplyTo = msg[1];
  replyTo.send('Yes I hear you: $msg, echoed from spawned isolate'),
  // 10- received END signal, close the ReceivePort to save         // resources:
  if (data == "END") {
  print('ISOL: my receivePort will be closed'),
  port.close();
  }
  });
}

Future sendReceive(SendPort port, msg) {
  ReceivePortrecv = new ReceivePort();
  port.send([msg, recv.sendPort]);
  returnrecv.first;
}

This script produces the following output:

ISOL: received [Do you hear me?,SendPort]

MAIN: received Yes I hear you: [Do you hear me?,SendPort], echoed from spawned isolate

ISOL: received [END, SendPort]

ISOL: my receivePort will be closed

From the output, we see that the main isolate receives its message echoed back from the second isolate. Examine the code in using_spawnuri.dart to create isolates with spawnUri:

import'dart:async';
import'dart:isolate';

main() {
  varrecv = new ReceivePort();
  Future<Isolate> remote =
  Isolate.spawnUri(Uri.parse("echo.dart"),
  ["Do you hear me?"], recv.sendPort);
  remote.then((_) =>recv.first).then((msg) {
  print("MAIN: received $msg");
  });
}

The following is the code from echo.dart:

import'dart:isolate';

void main(List<String>args, SendPortreplyTo) {
  replyTo.send(args[0]);
}

The following is the output:

MAIN: received Do you hear me?

How it works...

Isolates are defined in their own library called dart:isolate. They conform to the well-known actor-model: they are like separate little applications that only communicate with each other by passing asynchronous messages back and forth; in no way can they share variables in memory. The messages get received in the order in which you send them. Each isolate has its own heap, which means that all values in memory, including global variables, are available only to that isolate. Sending messages, which comes down to serializing objects across isolates, has to obey certain restrictions. The messages can contain only the following things:

  • Primitive values (null, bool, num, double, and String)
  • Instances of SendPort
  • Lists and maps whose elements are any of these

When isolates are created via spawn, they are running in the same process, and then it is also possible to send objects that are copied (currently only in the Dart VM).

An isolate has one ReceivePort to receive messages (containing data) on; it can listen to messages. Calling the sendport getter on this port returns SendPort. All messages sent through SendPort are delivered to the ReceivePort they were created from. On SendPort, you use the send method to send messages; a ReceivePort uses the listen method to capture these messages.

For each ReceivePort port there can be many SendPort. A ReceivePort is meant to live for as long as there is communication, don't create a new one for every message. Because Dart does not have cross-isolate garbage collection, ReceivePort is not automatically garbage-collected when nobody sends messages to them anymore.

Tip

Treat ReceivePort like resources, and close them when they aren't used anymore.

When working with isolates, a ReceivePort in the main or root isolate is obligatory.

Tip

Keeping the ReceivePort open will keep this main isolate alive; close it only when the program can stop.

Schematically, we could represent it as shown in the following diagram:

How it works...

A new isolate is created in one of the following two ways:

  • Through the static method Isolate.spawn(fnIsolate, msg), the new isolate shares the same code from which it was spawned. This code must contain a top-level function or static one-argument method fnIsolate, which is the code the isolate will execute (in our example, the echo function); msg is a message. In step 2, msg is the SendPort of the main isolate; this is necessary because the spawned isolate will not know where to send its results.
  • Through the static method Isolate.spawnUri(uriOfCode, List<String>args, msg), the new isolate executes the code specified in the Uri uriOfCode (in our example, the script echo.dart), and passes it the argument list args and a message msg (again containing the SendPort).

Isolates start out by exchanging SendPort in order to be able to communicate. Both methods return a Future with isolate, or an error of type IsolateSpawnException, which must be caught. This can be done by chaining a catchError clause or using the optional onError argument of spawn. However, an error occurring in a spawned isolate cannot be caught by the main isolate, which is logical, because both are independent code sets being executed. You can see this for yourself by running isolates_errors.dart. Keep in mind the following restrictions when using spawn and spawnUri:

  • Spawn works in server apps but doesn't work in Dart web apps. The browser's main isolate, the DOM isolate, does not allow this. This is meant to prevent concurrent access to the DOM.
  • However, spawnUri does work in Dart web apps and server apps, and the isolate resulting from this invocation can itself spawn other isolates. The Dart VM translates these isolates into web workers (refer to http://www.html5rocks.com/en/tutorials/workers/basics/).

There's more...

So, if you have a compute-intensive task to run inside your app, then to keep your app responsive, you should put the task into its own isolate or worker. If you have many such tasks, then how many isolates should you deploy? In general, when the tasks are compute-intensive, you should use as many isolates as you expect to have cores or processors available. Additional isolates that are purely computational are redundant. But if some of them also perform asynchronous calls (such as I/O for example), then they won't use much processor time. In that case, having more isolates than processors makes sense; it all depends on the architecture of your app. In extreme cases, you could use a separate isolate for each piece of functionality or to ensure that data isn't shared.

Tip

Always benchmark your app to check whether the number of isolates are optimized for the job. You can do this as follows: in the Run Manage Launches tool, tick the choices in the VM settings, pause isolate on start, and pause isolate on exit. Then, open the observatory tool through the image button on the left-hand side of Dart Editor to the red square for termination, where you can find interesting information about allocations and performance.

There's more...

As demonstrated in the second example, spawnUri provides a way to dynamically (that is, in run-time) load and execute code (perhaps even an entire library). Don't confuse Futures and isolates; they are different and are also applied differently.

  • An isolate is used when you want some code to truly run in parallel, such as a mini program running separately from your main program. You send isolate messages, and you can receive messages from isolates. Each isolate has its own event-loop.
  • A Future is used when you want to be notified when a value is available later in the event-loop. Just asking a Future to run a function doesn't make that function run in parallel. It just schedules the function onto the event-loop to be run at a later time.

At this moment, isolates are not very lightweight in the sense of Erlang processes, where each process only consumes a small amount of memory (of the order of Kb). Evolving isolates towards that ideal is a longer-term goal of the Dart team. Also, exception handling and debugging within isolates are a bit rough or difficult; expect this to change. It is also not specified how isolates map to operating system entities such as threads or processes; this can depend on the environment and platform. Isolates haven't been extended yet to inter-VM communication.

Tip

When two Dart VMs are running on the server, it is best to use TCP sockets for communication. You can start ServerSocket to listen for incoming requests and use Socket to connect to the other server.

See also

  • Refer to the Using isolates in web apps recipe for another example using spawnUri. Find another example of isolates in the Using multiple cores with isolates recipe.
  • Refer to the Using Sockets recipe for more information on Sockets and ServerSockets in the next chapter.
  • Refer to the Profiling and benchmarking your app recipe in Chapter 2, Structuring, Testing, and Deploying an Application, for more information on benchmarking.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset