How to get simple answer from isolate? - dart

I am learning about Isolate's. I read docs. And want to write minimal working example. Here is my code:
main() async
{
ReceivePort receivePort = ReceivePort();
Isolate.spawn(echo, receivePort.sendPort);
var sendPort = await receivePort.first;
}
echo(SendPort sendPort)
{
ReceivePort receivePort = ReceivePort();
sendPort.send(receivePort);
}
It almost ok, but I can't understand how I can send simple "Hello" message back. I looked few examples and there was some middle-ware like sendReceive(). Im I right understand that after:
var sendPort = await receivePort.first;
sendPort will store name/address of spawned function and I need sendPort.send("hello");?

You already stated how to send simple data using SendPort.send. In fact, you are only able to send primitive data types, i.e. null, num, bool, double, String as described in the documentation.
I will complete your example in the following.
import 'dart:isolate';
main() async {
final receivePort = ReceivePort();
await Isolate.spawn(echo, receivePort.sendPort);
final Stream receivePortStream = receivePort.asBroadcastStream();
receivePortStream.listen((message) {
if (message is String) {
print('Message from listener: $message');
} else if (message is num) {
print('Computation result: $message');
}
});
final firstMessage = await receivePortStream.first;
print(firstMessage);
}
echo(SendPort sendPort) {
sendPort.send('hello');
sendPort.send('another one');
final computationResult = 27 * 939;
sendPort.send(computationResult);
}
Note that you want to simply send 'hello' and not some other ReceivePort, which will not even work as it is not a primitive value.
In my example, I also setup another listener that will process further messages.
Additionally, I needed to create the receivePortStream variable as a broadcast stream in order to be able to both listen to it and get the first message. If you try to run ReceivePort.listen and ReceivePort.first on the same ReceivePort, you will get an exception.

Related

why do we have to exit from main with using exit(0) after using Isolates?

import 'dart:io';
import 'dart:isolate';
Isolate? isolate;
void printX(SendPort sendPort) {
print(sendPort);
}
void main() async {
var receiverPort = ReceivePort();
isolate = await Isolate.spawn(printX, receiverPort.sendPort);
isolate!.kill(priority: Isolate.immediate);
exit(0);
}
why do we have to do exit(0)?
I saw that if I do not exit with exit code then I get stuck like it is waiting for some input.
although the isolate is killed.
It's not the Isolate that's keeping the process alive, per se; it's actually the ReceivePort. The ReceivePort doesn't "know" that the isolate has been killed, so it's still happily waiting for events to come along.
Calling receiverPort.close() is what will allow the process to end. In fact, you don't even technically need to kill the isolate in order for the process to end, as long as you close the stream.
Here's a full version of your code which terminates immediately:
import 'dart:io';
import 'dart:isolate';
Isolate? isolate;
void printX(SendPort sendPort) {
print(sendPort);
}
void main() async {
var receiverPort = ReceivePort();
isolate = await Isolate.spawn(printX, receiverPort.sendPort);
receiverPort.close();
}
Additional note: If there is a listener subscribed to the ReceivePort, and the listener cancels its subscription, then the ReceivePort will close on its own; however, it's designed to buffer incoming events until a listener is subscribed - so if no listener ever subscribes, it never closes itself.
So this code will also terminate:
void main() async {
var receiverPort = ReceivePort();
var subscription = receiverPort.listen((message) {});
isolate = await Isolate.spawn(printX, receiverPort.sendPort);
subscription.cancel();
}

How to pass arguments (besides SendPort) to a spawned isolate in Dart

In this article, they spawned an isolate like this:
import 'dart:isolate';
void main() async {
final receivePort = ReceivePort();
final isolate = await Isolate.spawn(
downloadAndCompressTheInternet,
receivePort.sendPort,
);
receivePort.listen((message) {
print(message);
receivePort.close();
isolate.kill();
});
}
void downloadAndCompressTheInternet(SendPort sendPort) {
sendPort.send(42);
}
But I can only pass in the receive port. How do I pass in other arguments?
I found an answer so I'm posting it below.
Since you can only pass in a single parameter, you can make the parameter a list or a map. One of the elements is the SendPort, the other items are the arguments you want to give the function:
Future<void> main() async {
final receivePort = ReceivePort();
final isolate = await Isolate.spawn(
downloadAndCompressTheInternet,
[receivePort.sendPort, 3],
);
receivePort.listen((message) {
print(message);
receivePort.close();
isolate.kill();
});
}
void downloadAndCompressTheInternet(List<Object> arguments) {
SendPort sendPort = arguments[0];
int number = arguments[1];
sendPort.send(42 + number);
}
You've lost type safety like this, but you can check the type in the method as needed.
We can get back the type safety by creating a custom class with fields
class RequiredArgs {
final SendPort sendPort;
final int id;
final String name;
RequiredArgs(this.sendPort, this.id, this.name);
}
void downloadAndCompressTheInternet(RequiredArgs args) {
final sendPort = args.sendPort;
final id = args.id;
final name = args.name;
sendPort.send("Hello $id:$name");
}
Code will be much cleaner and safer in this way 🥸

Bidirectional communication with isolates in Dart 2

I'm trying isolates and I'm wondering how could I spawn some of them doing heavy computations that, when the root Isolate ask them for their current computing value they respond it, "on demand".
As far as I know, the only object that can be used as message for the newly created isolates is SendPort, meaning that only the spawned isolate can communicate with the root one. I tried sending a < SendPort,ReceivePort> tuple, but as ReceivePort isn't a SendPort, it's considered as illegal.
In a nutshell:
root <-- isolate good
root <-> isolate how to?
With Gunter's comment I made this:
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
Stopwatch stopwatch = new Stopwatch();
main(args) async {
ReceivePort rPort = new ReceivePort();
rPort.listen((data) {
print("<root> $data received");
if (data is List) {
String action = data[0];
if (action == "register") {
(data[1] as SendPort).send(stopwatch.elapsedMilliseconds);
}
}
});
stopwatch.start();
await Isolate.spawn(elIsolate, rPort.sendPort);
print("isolate spawned in ${stopwatch.elapsedMilliseconds} msecs"); //isolate spawned in 377 msecs
}
void elIsolate(SendPort sPort) {
ReceivePort rPort = new ReceivePort();
rPort.listen((data) {
print("<Isolate> '$data' received"); //<Isolate> '387' received
});
sPort.send(["register", rPort.sendPort]);
}
While with Kevin's answer the code simplified to:
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:stream_channel/stream_channel.dart';
Stopwatch stopwatch = new Stopwatch();
main(args) async {
ReceivePort rPort = new ReceivePort();
IsolateChannel channel = new IsolateChannel.connectReceive(rPort);
channel.stream.listen((data) {
print("<root> '$data' received at ${stopwatch.elapsedMilliseconds} msecs"); //<root> 'hello world' received at 1141 msecs
channel.sink.add(stopwatch.elapsedMilliseconds);
});
stopwatch.start();
await Isolate.spawn(elIsolate, rPort.sendPort);
print("isolate spawned in ${stopwatch.elapsedMilliseconds} msecs"); //isolate spawned in 1111 msecs
}
void elIsolate(SendPort sPort) {
IsolateChannel channel = new IsolateChannel.connectSend(sPort);
channel.stream.listen((data) {
print("<Isolate> '$data' received");
});
channel.sink.add("hello world");
}
Look at IsolateChannel from the package:stream_channel.
This should provide a LOT of help for what you're trying to do.

Isolate code didn't work as expected

Expected "Hello world" from simple isolate code shown below & didn't work.
import 'dart:async';
import 'dart:isolate';
var mainReceivePort = new ReceivePort();
main() async {
await Isolate.spawn(hello, null);
await for (var msg in mainReceivePort) {
print(msg);
return;
}
}
hello(_) async {
var sendPort = mainReceivePort.sendPort;
sendPort.send("Hello world");
}
When following changes were made to the code, it works as intended
import 'dart:async';
import 'dart:isolate';
var mainReceivePort = new ReceivePort();
main() async {
await Isolate.spawn(hello, mainReceivePort.sendPort);
await for (var msg in mainReceivePort) {
print(msg);
return;
}
}
hello(sendPort) async {
sendPort.send("Hello world");
}
Looking for clues. any thoughts?
In the first example sendPort is not connected to the main isolate, it exists only in the spawned isolate.
This code is executed in both isolates
var mainReceivePort = new ReceivePort();
and each isolate gets a different mainReceivePort instance which are not connected in any way.
In the 2nd example the sendPort connected to mainReceivePort of the main isolate is passed to the spawned isolate and messages passed to it will be received by the connected mainReceivePort of the main isolate.

Is there any example for dart's `spawnUri(...)` in library "dart:isolate"?

There is a spawnUri(uri) function in dart:isolate, but I don't find any example. I have guessed its usage, but failed.
Suppose there are 2 files, in the first one, it will call spawnUri for the 2nd one, and communicate with it.
first.dart
import "dart:isolate";
main() {
ReceivePort port = new ReceivePort();
port.receive((msg, _) {
print(msg);
port.close();
});
var c = spawnUri("./second.dart");
c.send(["Freewind", "enjoy dart"], port.toSendPort());
}
second.dart
String hello(String who, String message) {
return "Hello, $who, $message";
}
void isolateMain(ReceivePort port) {
port.receive((msg, reply) => reply.send(hello(msg[0], msg[1]));
}
main() {}
But this example doesn't work. I don't know what's the correct code, how to fix it?
Here is a simple example that works with Dart 1.0.
app.dart:
import 'dart:isolate';
import 'dart:html';
import 'dart:async';
main() {
Element output = querySelector('output');
SendPort sendPort;
ReceivePort receivePort = new ReceivePort();
receivePort.listen((msg) {
if (sendPort == null) {
sendPort = msg;
} else {
output.text += 'Received from isolate: $msg\n';
}
});
String workerUri;
// Yikes, this is a hack. But is there another way?
if (identical(1, 1.0)) {
// we're in dart2js!
workerUri = 'worker.dart.js';
} else {
// we're in the VM!
workerUri = 'worker.dart';
}
int counter = 0;
Isolate.spawnUri(Uri.parse(workerUri), [], receivePort.sendPort).then((isolate) {
print('isolate spawned');
new Timer.periodic(const Duration(seconds: 1), (t) {
sendPort.send('From app: ${counter++}');
});
});
}
worker.dart:
import 'dart:isolate';
main(List<String> args, SendPort sendPort) {
ReceivePort receivePort = new ReceivePort();
sendPort.send(receivePort.sendPort);
receivePort.listen((msg) {
sendPort.send('ECHO: $msg');
});
}
Building is a two-step process:
pub build
dart2js -m web/worker.dart -obuild/worker.dart.js
See the complete project here: https://github.com/sethladd/dart_worker_isolates_dart2js_test
WARNING : This code is out of date.
Replace your second.dart with the following to make it work :
import "dart:isolate";
String hello(String who, String message) {
return "Hello, $who, $message";
}
main() {
port.receive((msg, reply) => reply.send(hello(msg[0], msg[1])));
}
This gist: https://gist.github.com/damondouglas/8620350 provides a working (I tested it) Dart 1.5 example. An Isolate.spawn(...) example can be found there as well.
Reproducing here (adding import statements):
echo.dart:
import 'dart:isolate';
void main(List<String> args, SendPort replyTo) {
replyTo.send(args[0]);
}
main.dart:
import 'dart:isolate';
import 'dart:async';
main() {
var response = new ReceivePort();
Future<Isolate> remote = Isolate.spawnUri(Uri.parse("echo.dart"), ["foo"], response.sendPort);
remote.then((_) => response.first)
.then((msg) { print("received: $msg"); });
}
shameless copied from
Dart Web Development › Example on how to use Isolate.spawn
I hope the author doesn't mind
The spawned isolate has no idea where/how to respond to its parent.
In the parent, you could create a ReceivePort which will receive all message from child isolates.
Whenever you spawn an isolate, pass it the SendPort instance from your ReceivePort (via the message argument of Isolate.spawn).
The child isolate may/should create its own ReceivePort as well, so it can receive messages.
When instantiated, the child isolate must send its own SendPort (from its own ReceivePort) to its parent (via the parent's SendPort).
The current API is, in its own, really not helpful. But it provides all the necessary building blocks for a full-blown implementation.
You may need to wrap messages inside headers, something along these lines:
class _Request {
/// The ID of the request so the response may be associated to the request's future completer.
final Capability requestId;
/// The SendPort we must respond to, because the message could come from any isolate.
final SendPort responsePort;
/// The actual message of the request.
final dynamic message
const _Request(this.requestId, this.responsePort, this.message);
}
class _Response {
/// The ID of the request this response is meant to.
final Capability requestId;
/// Indicates if the request succeeded.
final bool success;
/// If [success] is true, holds the response message.
/// Otherwise, holds the error that occured.
final dynamic message;
const _Response.ok(this.requestId, this.message): success = true;
const _Response.error(this.requestId, this.message): success = false;
}
Every isolate could have a singleton message bus like this:
final isolateBus = new IsolateBus();
class IsolateBus {
final ReceivePort _receivePort = new ReceivePort();
final Map<Capability, Completer> _completers = {};
IsolateBus() {
_receivePort.listen(_handleMessage, onError: _handleError);
}
void _handleMessage(portMessage) {
if (portMessage is _Request) {
// This is a request, we should process.
// Here we send back the same message
portMessage.responsePort.send(
new _Response.ok(portMessage.requestId, portMessage.message));
} else if (portMessage is _Response) {
// We received a response
final completer = _completers[portMessage.requestId];
if (completer == null) {
print("Invalid request ID received.");
} else if (portMessage.success) {
completer.complete(portMessage.message);
} else {
completer.completeError(portMessage.message);
}
} else {
print("Invalid message received: $portMessage");
}
}
void _handleError(error) {
print("A ReceivePort error occured: $error");
}
Future request(SendPort port, message) {
final completer = new Completer();
final requestId = new Capability();
_completers[requestId] = completer;
port.send(new _Request(requestId, _receivePort.sendPort, message));
return completer.future;
}
}
SendPort anotherIsolatePort = ...
isolateBus.request(anotherIsolatePort, "Some message");
This is just one architectural example. You could of course roll-out your own.
This could be extended to support notifications (requests without response), streams, etc.
A global isolate registry could be needed to keep track of all SendPort instances from every isolates and eventually register them as services.

Resources