Monday, January 18, 2016

A Naming Convention for Dart Isolate Ports


I still can't isolate. Well, I can create isolate workers in Dart, but they feel incredibly awkward to use to support programming discussions. I may give up on them, but I'd like at least one more shot at them.

On the face of it, communication between the main entry point and a worker is fairly simple:



Main sends messages from its send-port to the receive-port in the worker. The worker uses its own send-port to send messages to the receive-port back in main. Simple, right? Yes and no.

From the above diagram, you might think that main's receive-port is created last:
  1. first you need a send-port to send to the worker
  2. second, the worker needs a receive port to listen for those messages
  3. third, the worker needs a send-port to send back to main
  4. last, main needs the receive port to listen for messages from worker
In reality, Dart receive-ports are created first. A send-port is just a property of the receive-port. As a property of a receive-port, the send-port is already linked for communication, the challenge is then to get main's receive-port send-port to the worker and vice-versa.

Side-note: sentences like the last one are probably why I will not be able to use isolates in discussions like remote proxy patterns. It makes sense if you noodle it through, but readers should expend cognitive load on the main discussion, not the apparatus for the discussion. Anyway, onward...

To create an isolate worker, the main entry point first creates its receive-port (with associated send-port). It then spawns the worker sending along the associated send-port at the same time:



At this point, the worker can send all the messages it likes back to the main worker, but main has no way to communicate back to worker. In many cases this is just fine. In many of the examples that I want to use, however, this is insufficient. To allow main to communicate with worker, worker has to create its own receive-port and supply the associated send-port back to main. There is only one way to do so—back through main's send-port:



All of this makes perfect sense. I understand the tradeoffs involved. I understand how to set it up. I cannot think of better names than "ports" for these beasties. But the end result is that I have to send a send-port over a send-port in order to establish worker's receive-port. And all I really want is to discuss the proxy pattern, darn it.

I am unsure how to proceed at this point. It seems like a higher level library is in order, but then I have a library just for teaching purposes. Maybe that is what I will wind up doing in the end. First though, I am going to experiment with a worker-centric naming convention to see if it helps the actual code.

So, in worker, I will refer to the send-port (which comes from main) as "worker-out." Back in main, that same send-port will be associated with "main-in":



To make that happen, I start in main() by creating my mainIn receive-port, then sending its sendPort to worker() when it is spawned:
main() async {
  var mainIn = new ReceivePort();
  await Isolate.spawn(worker, mainIn.sendPort);
  // ...
}
(I am using the async / await syntactic sugar for Dart futures here)

So far, so good. I have a good handle on what mainIn is. Previously, I had called that receivePort or just r—by the time I was looking inside worker, I was easily confused. Hopefully this naming convention will serve me better.

Then, down in the worker() that is being spawned, I accept main's mainIn.sendPort, assigning it locally as workerOut:
worker(SendPort workerOut) {
  // ...
}
At the risk of being redundant, from main's perspective, this is mainIn.sendPort. From worker's perspective, that same thing is workerOut. I think that works.

Now I need a workerIn, which is a receive-port and I need to send it back to main:
worker(SendPort workerOut) {
  var workerIn = new ReceivePort();
  workerOut.send(workerIn.sendPort);
  // ...
}
Lastly, back in main, I need to accept that first message and assign it as mainOut. I cannot just ask mainIn for the first message because that has the side-effect of closing the stream and all of this bi-directional communication setup would be for naught. Instead, I convert mainIn from a receive-port to a broadcast stream using the asBroadcastStream() method:
main() async {
  var mainIn = new ReceivePort();
  await Isolate.spawn(worker, mainIn.sendPort);

  var inStream = mainIn.asBroadcastStream();
  SendPort mainOut = await inStream.first;
  // ...
}
With that, I can still listen to inStream for additional communication—even after I have mainOut. Conceptually, this winds up looking something like:



I think I am OK with that. Renaming the ports after the context seems to help clear up most of my confusion. I will likely adopt this approach in future isolate code. That said, I remain unconvinced that this is clear enough for something like Design Patterns in Dart. I may try my hand at a high-level, simple library. I believe that I have already searched for one, but I may also check to see if any existing libraries might suit my needs.

Grist for tomorrow...


Day #68

1 comment:

  1. I guess the other alternative is toWorker, fromWorker, toMain, and fromMain. This would allow toWorker1, fromWorker1 if there are more than one worker involved. 6 of 1 ... :)

    ReplyDelete