-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make send-and-exit a public API #47164
Comments
I'd love to see this API in the September beta (which goes out in October) so we could potentially get some feedback prior to going to stable. Is that possible? Tagged for now, let me know if we should change. |
I'll start brainstorming. As I understand it, It's basically: /// Sends [message] to [port] and synchronously terminates the current isolate.
static Never sendAndExit(SendPort port, Object? value) {
port.send(value); // If port isn't a native send-port
Isolate.exit(); // Synchronously kills the current isolate, like `Process.exit` would for the process.
} I'd make it static method, not an instance method on With that, we should also add static Never killCurrentIsolate() {
sendAndExit(RawReceivePort().sendPort, null);
} So, maybe we should instead make class Isolate {
...
/// Terminates the current isolate synchronously.
///
/// This operations is potentially dangerous and should be used judiciously.
/// The isolate stops operating *immediately*. A call to this message does not return and
/// also doesn't throw. Pending `finally` blocks are not executed,
/// control flow will not go back to the event loop,
/// scheduled asynchronous asks will never run,
/// and even pending isolate control commands may be ignored.
/// (The isolate will send messages to ports already registered using [Isolate.addOnExitListener],
/// but no further Dart code will run in the isolate.)
///
/// If [finalMessagePort] is provided, the [message] is sent to that port
/// as the final operation of the current isolate.
/// (If the port is a native port, one provided by [ReceiverPort.sendPort] or [RawReceivePort.sendPort],
/// the system may be able to send this final message more efficiently than normal port communication
/// between live isolates.)
external static Never exit([SendPort finalMessagePort, Object? message]); So, basically, the operation is "exit (and maybe send)" instead of "send and exit". Since exiting is the one guaranteed side-effect, and sending can go wrong for any number of reasons (like using a send port linked to an already closed receive port), making So, that's my initial API suggestion: (We can't make the Considerations: Q. What if I want to send the message to more than one port? It's still incredibly dangerous to just drop all Another alternative could be to have Scheduling two exits will be an error (unless we provide a way to cancel it again, which may be useful. Imagine the horror of a program knowing that it has scheduled an exit, and desperately trying to keep coming up with microtasks, just to stay alive! Or don't anthropomorphise programs. Anyway ...). Then the That should be easily implementable. Should be named something like: class Isolate {
...
/// Schedules the isolate to terminate as soon as possible.
///
/// Sets the current isolate to terminate as if by receiving an [Isolate.kill] command.
/// Must only be called once.
///
/// If [finalMessagePort] is provided, the [message] is sent to that port when the isolate has terminated,
/// just like ports and messages registered using [Isolate.addOnExitListener].
/// (If the port is a native port, one provided by [ReceiverPort.sendPort] or [RawReceivePort.sendPort],
/// the system may be able to send this final message more efficiently than normal port communication
/// between live isolates.)
external static void scheduleExit([SendPort? finalMessagePort, Object? message]);
} Piggy-backing on |
Awesome brainstorm @lrhn ! Only other consideration I have is that having |
related: #37835 |
I would probably agree with @lrhn about finally blocks not running being problematic. For example, you might be using a |
From the other thread, the concern about whether an object gets collected on a different thread probably isn't a huge deal - the VM already does that to us anyway, e.g. when running a multi-threaded collection I've observed the object finalizers getting run on a VM worker thread rather than the mutator thread. |
Another reason to have |
Perhaps sendAndKill would be a clearer name then? Alternatively, if possible, have some kind of parameter to sendAndExit to control whether it's killed or allowed to finish anything. |
We tried alternative along that line of thinking, but it was dismissed early on.
If at all possible I think it would be better to avoid parameters customizing this kind of behavior to keep things simpler for user. sendAndExit has immediate aspect, which is to verify whether message object can be actually sent, then there is potentially-delayed aspect of an isolate exiting and message passing. If we let dart user code run between these two points, message object might be updated so it no longer passes verification check for example. |
The word As mentioned before, we can make it delay the send until being back at the event loop, but that only ensures evaluation of synchronous |
If you are fine with inconsistency with |
Agree, and as I mentioned, it only runs synchronous finally clauses, so it's only removing some of the risk of an early exit, not all of it. So, exiting immediately is probably fine. |
Just to be sure - Flutter's If we changed line 98 to use sendAndExit, and then removed the call to |
Great. In that case I'm fine with this. I think if anything we'd just add a comment in the implementation to remind ourselves that we have to make sure any important work is finished because anything that's unawaited or in a finally block will not execute after sendAndExit. |
(Flutter's Oh, well, now that I looked at it, maybe try this alternative (completely untested, naturally): Future<R> compute<Q, R>(FutureOr<R> Function(Q) callback, Q message, { String? debugLabel }) async{
debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
final Flow flow = Flow.begin();
Timeline.startSync('$debugLabel: start', flow: flow);
final ReceivePort messagePort = ReceivePort();
Timeline.finishSync();
final Isolate isolateFuture = await Isolate.spawn<_IsolateConfiguration<Q, FutureOr<R>>>(
_spawn,
_IsolateConfiguration<Q, FutureOr<R>>(
callback,
message,
messagePort.sendPort,
debugLabel,
flow.id,
),
errorsAreFatal: true,
onExit: messagePort.sendPort,
onError: messagePort.sendPort,
);
var message = await messagePort.first; // Closes port after first message.
Timeline.startSync('$debugLabel: end', flow: Flow.end(flow.id));
if (message == null) {
// An onExit handler message.
Timeline.finishSync();
throw Exception('Isolate exited without result or error.'));
}
assert(message is List<dynamic>);
var list = message as List<dynamic>;
if (list.length == 1) {
// A result message.
var result = list[0] as R;
isolate.kill();
Timeline.finishSync();
return result;
}
assert(list.length == 2);
// An onError handler message.
// (Consider using `RemoteError(list[0] as String, list[1] as String)` instead
// That's what `Isolate.errors` uses for remote errors.)
final Exception exception = Exception(list[0]);
final StackTrace stack = StackTrace.fromString(list[1] as String);
Timeline.finishSync();
await Future<Never>.error(exception, stack);
}
@immutable
class _IsolateConfiguration<Q, R> {
const _IsolateConfiguration(
this.callback,
this.message,
this.resultPort,
this.debugLabel,
this.flowId,
);
final isolates.ComputeCallback<Q, R> callback;
final Q message;
final SendPort resultPort;
final String debugLabel;
final int flowId;
FutureOr<R> apply() => callback(message);
}
Future<void> _spawn<Q, R>(_IsolateConfiguration<Q, FutureOr<R>> configuration) async {
final R result = await Timeline.timeSync(
configuration.debugLabel,
configuration.apply,
flow: Flow.step(configuration.flowId),
);
Timeline.timeSync(
'${configuration.debugLabel}: returning result',
() { configuration.resultPort.send(List<Object?>.filled(1, result)); },
flow: Flow.step(configuration.flowId),
);
} ) |
Filed flutter/flutter#90693 |
Hey @aam, can we please add an entry for this in the |
Perfect, thanks! (and sorry my search skills failed me) |
Isolate groups were enabled by-default in the September beta (though it's still possible to disable it via the flag).
In the October or November beta we're going to remove the flag entirely and make it the only option (if we don't hear anything concerning until then). As part of that we should make send-and-exit a public API.
@aam Could you work with @lrhn on this?
The text was updated successfully, but these errors were encountered: