Dart Isolates

von

Dart is very young but also a very promising language. You can write it similar to JavaScript, but it can also look like Java. In JavaScript there is no real multithreading. Just the Chrome Browser currently does some magic and spawns multiple threads for some tasks. But a developer cannot rely “maybe” and “probably”. Dart fixes the situation. With Dart you can have some kind of multithreading. What Java has with Threads, Dart has with so called “Isolates”. Isolates are very cool. They have been highly inspired by Erlang and it seems there is a collaboration between Erlang people and the Dart devs. To create Isolates you need to know about the following classes:

Isolates are fully independent from each other. It means, they do not share any variables - no static and no members or anything else. You can only communicate between Isolates through so called ports. This has a good benefit - you might be able to restart parts of your program if something goes wrong. But you need to take care on the two flavors of Isolates explained later.

An Isolate lives as long as its ports are open. If you close the port, the Isolate might go away. You cannot check if it actually goes away from within Dart at the moment, you’ll need to trust Dart VM.

Let’s look at it more in detail now. Our plan is to let one class “Starter” start another Isolate named “Worker”.

Creating the worker class - extending Isolate

The Worker class is actually pretty simple. It looks like that:

class Worker extends Isolate {
  Worker() : super.heavy();

  main() {
    this.port.receive(
          void _(var message, SendPort replyTo) {
            print ("Worker receives: ${message}");
            replyTo.send("Pong");
        this.port.close();
          }
      );
  }
}

As you can see, the Worker class extends Isolate. In the next line we call the super constructor. In fact it is a named constructor called “heavy”. I will care later on the difference between “light” and “heavy” Isolates.

Once an Isolate starts its service as an own thread (it is spawned - look at the Starter class below how this is done) the inherited field “port” get populated with an ReceivePort. It means, the Isolate can get messages from another Isolate through this port. Therefore it might be rather important to add a function to “receive()”. Otherwise you will just run the main() method and that’s it. In worst case you even leave the this.port open and that newly created Isolate will never go away.

Therefore it might be a good idea to put just necessary initialization code into the main() method and then implement a method which is called on receive of a message. In addition this method needs to take care on closing it’s port.

This is actually what I have done in my Worker class. You can see the main() method and that I call receive() with an anonymous function. From now on this function will be called when a message arrives.

Looking at my anonymous function you see I get the “message” param, which might be anything: it is dynamic. Second parameter is a SendPort. Actually this gives you the chance to respond to the Caller. I actually do this, sending a “Pong” message back.

So this was pretty straightforward. Now let’s look on the other class, which should spawn the worker.

Creating the Starter class

Here is the code:

class Starter {
  ReceivePort _receivePort;

  Starter.start() : 
    _receivePort = new ReceivePort() {
      this._receivePort.receive(
        void _(var message, SendPort replyTo) {
              print ("Receiving from Worker: ${message}");
              _receivePort.close();
            }
      );

      Worker worker = new Worker();
      worker.spawn().then((SendPort port) {
        port.send('Ping', _receivePort.toSendPort());
    });   
  }
}

OK, looks a bit more complicated at first glance, but it isn’t. I created a Standard class called “Starter” with a field _receivePort. I want to use it to get some responses from the Isolates I create from here.

Then I define my named Constructor “start()” and initialize the ReceivePort in line 5. Similar to the Worker class, I put a function into my ReceivePort which should be called once I get a response to this port. In fact, the “Pong” message from the Worker will arrive here.

In line 13 I create a new Object of the Worker. No magic, nothing happens. The actual Worker becomes a true Isolate when spawn() is called in line 14. After the spawn has created the new Isolate, “then()” will be called with a SendPort. Surprise - the SendPort is the port which has been populated by Dart in Worker.port.

Now you can use this SendPort to send a message. I do this in line 15. I send the message and need to give a SendPort to my Worker. ReceiverPort fortunately has the method toSendPort() which creates the backchannel.

main() {
  new Starter.start();
}

Now you you’ll see the outcome. The Program should exit after the messages have been sent around. If it doesn’t you have probably open ports around.

Difference between light and heavy Isolates

If you create an Isolate, you have the choice between “light” and “heavy”. The first one is the default. So, what is the difference? Actually both start an Isolate with fresh static state and both can only communicate via ports. And, best of all, both flavors are asynchronous.

The difference is, light Isolates live in the same thread as the creating Isolate. One could say, this is basically like in JavaScript. It means only one execution can happen at one time. If there is an message, then it will be queued until the currently running Isolate has finished and the next one can put it from the Queue.

Heavy Isolates on the other hand create a real new Thread. It is an actual process and now things can be done in parallel. After all even Heavy Isolates get messages in their queue, but they work on the next message only after they have finished the current loop. This is appealing, because you don’t need to think about threading concurrency issues. On the other hand, if you need some performance, you probably need some more Isolates. But spawning a new process does always have some kind of performance cost. It might make sense for some applications on the server side to create an Isolate pool when the VM starts.

And hey, you should know about some more stuff, before using it…

Sidenotes

Dart is very young and you can expect changes in the area of Isolates. At the moment difference between light and heavy is true for DartC (and only if Webworkers are available).

At the VM side every Isolate will be created as a new native thread. In other terms, there is no difference between light and heavy threads.

As mentioned, there is already some discussion on the dev side about Isolates. It might happen that light and heavy will disappear completely and something else will replace it. Even the idea of using a threadpool for light Isolates exists (very appealing, honestly).

Thanks very much to the friendly Google Devs who helped me understand Isolates better!

Tags: #Dart #Open Source

Newsletter

ABMELDEN