Perhaps the best answer is "not."
I have been struggling with how best to present isolates (isolated workers) as a remote proxy pattern vehicle. As of last night, I think I have the bare minimum of what I can do in my Dart code. It's good—functional, well named, proper—but still complex enough that it would distract from the main discussion.
So what if I get rid of isolates altogether? Well, for one thing, I likely would not really need a remote proxy pattern implementation. The main function could speak directly to the worker function without required go-betweens like send and receive ports. For the sake of argument and illustration, let's stipulate that there is a requirement for main and worker functions to speak only over streams.
I start by replacing the isolate / send-port / receive-post dance with two stream controllers in
main()
—one for sending messages to the worker function, the other for receiving messages from the worker:main() async { var mainOut = new StreamController.broadcast(), mainIn = new StreamController.broadcast(); // ... }Next, I create a remote
ProxyCar
instance, supplying these two stream controllers for communication with the real subject (which will reside in the worker()
function):main() async { var mainOut = new StreamController.broadcast(), mainIn = new StreamController.broadcast(); // Create proxy car with send/receive streams ProxyCar car = new ProxyCar(mainOut, mainIn); // ... }This requires two minor changes to the
ProxyCar
declaration, neither of which should really affect ease of understanding. First, the _in
and _out
instance variables become StreamController
s instead of ReceivePort
and SendPort
. Second, I need to listen on the StreamController
's stream
instead of directly on the StreamController
object:class AsyncCar implements AsyncAuto { StreamController _out, _in; AsyncCar(this._out, this._in) { _in.stream.listen((message) { print("[AsyncCar] $message"); if (message == #drive) _car.drive(); if (message == #stop) _car.stop(); _out.add(state); }); } // Proxied methods remain unchanged... }Back in
main()
, I also sent the mainOut
and mainIn
stream controllers to the worker: // Start "worker"
worker(mainIn, mainOut);
Inside the worker()
function, these two arguments are mirrors of the arguments in main()
—mainIn
in main()
is workerOut
inside worker()
:worker(StreamController workerOut, StreamController workerIn) { new AsyncCar(workerOut, workerIn); }The
AsyncCar
class requires the same minor StreamController
changes that I made to ProxyCar
, but the actual functionality remains unchanged from the isolate version of the code.And that does the trick. Back in
main()
, I can invoke the usual vehicle methods on the ProxyCar
and those requests are forwarded onto the real AsyncCar
in worker()
:main() async { var mainOut = new StreamController.broadcast(), mainIn = new StreamController.broadcast(); ProxyCar car = new ProxyCar(mainOut, mainIn); worker(mainIn, mainOut); await car.drive(); print("Car is ${car.state}"); print("--"); await car.stop(); print("Car is ${await car.state}"); }Along with some debugging code inside the car classes, this code produces the following output:
$ ./bin/drive.dart [AsyncCar] Symbol("drive") [ProxyCar] driving Car is driving -- [AsyncCar] Symbol("stop") [ProxyCar] idle Car is idleI like that. The example is certainly contrived, but experienced Dartisans will recognize where this can go while folks new to the language should not be completely lost. Once the main discussion is done, I would then be free to show a quick code transformation into an isolate—or even a web socket—solution.
And best of all is that this approach can be seen on DartPad: https://dartpad.dartlang.org/7cc9f080e49939ac4cad.
Day #69
No comments:
Post a Comment