Add `Isolate.run`.
Change-Id: I049f6b1bf684ed28b6f14d380adfadf9f4e97fcb
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/217008
Reviewed-by: Nate Bosch <nbosch@google.com>
Reviewed-by: Michael Thomsen <mit@google.com>
Commit-Queue: Lasse Nielsen <lrn@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70414c2..a5313a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -63,6 +63,10 @@
- Deprecate `SecureSocket.renegotiate` and `RawSecureSocket.renegotiate`,
which were no-ops.
+#### `dart:isolate`
+
+- Add `Isolate.run` to run a function in a new isolate.
+
### Tools
#### Dart command line
diff --git a/sdk/lib/isolate/isolate.dart b/sdk/lib/isolate/isolate.dart
index 48c046f..517beee 100644
--- a/sdk/lib/isolate/isolate.dart
+++ b/sdk/lib/isolate/isolate.dart
@@ -144,6 +144,106 @@
/// inspect the isolate and see uncaught errors or when it terminates.
Isolate(this.controlPort, {this.pauseCapability, this.terminateCapability});
+ /// Runs [computation] in a new isolate and returns the result.
+ ///
+ /// ```dart
+ /// int slowFib(int n) =>
+ /// n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
+ ///
+ /// // Compute without blocking current isolate.
+ /// var fib40 = await Isolate.run(() => slowFib(40));
+ /// ```
+ ///
+ /// If [computation] is asynchronous (returns a `Future<R>`) then
+ /// that future is awaited in the new isolate, completing the entire
+ /// asynchronous computation, before returning the result.
+ ///
+ /// ```dart
+ /// int slowFib(int n) =>
+ /// n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
+ /// Stream<int> fibStream() async* {
+ /// for (var i = 0;; i++) yield slowFib(i);
+ /// }
+ ///
+ /// // Returns `Future<int>`.
+ /// var fib40 = await Isolate.run(() => fibStream().elementAt(40));
+ /// ```
+ ///
+ /// If [computation] throws, the isolate is terminated and this
+ /// function throws the same error.
+ ///
+ /// ```dart import:convert
+ /// Future<int> eventualError() async {
+ /// await Future.delayed(const Duration(seconds: 1));
+ /// throw StateError("In a bad state!");
+ /// }
+ ///
+ /// try {
+ /// await Isolate.run(eventualError);
+ /// } on StateError catch (e, s) {
+ /// print(e.message); // In a bad state!
+ /// print(LineSplitter.split("$s").first); // Contains "eventualError"
+ /// }
+ /// ```
+ /// Any uncaught asynchronous errors will terminate the computation as well,
+ /// but will be reported as a [RemoteError] because [addErrorListener]
+ /// does not provide the original error object.
+ ///
+ /// The result is sent using [exit], which means it's sent to this
+ /// isolate without copying.
+ ///
+ /// The [computation] function and its result (or error) must be
+ /// sendable between isolates.
+ ///
+ /// The [debugName] is only used to name the new isolate for debugging.
+ @Since("2.17")
+ static Future<R> run<R>(FutureOr<R> computation(), {String? debugName}) {
+ var result = Completer<R>();
+ var resultPort = RawReceivePort();
+ resultPort.handler = (response) {
+ resultPort.close();
+ if (response == null) {
+ // onExit handler message, isolate terminated without sending result.
+ result.completeError(
+ RemoteError("Computation ended without result", ""),
+ StackTrace.empty);
+ return;
+ }
+ var list = response as List<Object?>;
+ if (list.length == 2) {
+ var remoteError = list[0];
+ var remoteStack = list[1];
+ if (remoteStack is StackTrace) {
+ // Typed error.
+ result.completeError(remoteError!, remoteStack);
+ } else {
+ // onError handler message, uncaught async error.
+ // Both values are strings, so calling `toString` is efficient.
+ var error =
+ RemoteError(remoteError.toString(), remoteStack.toString());
+ result.completeError(error, error.stackTrace);
+ }
+ } else {
+ assert(list.length == 1);
+ result.complete(list[0] as R);
+ }
+ };
+ try {
+ Isolate.spawn(_RemoteRunner._remoteExecute,
+ _RemoteRunner<R>(computation, resultPort.sendPort),
+ onError: resultPort.sendPort,
+ onExit: resultPort.sendPort,
+ errorsAreFatal: true,
+ debugName: debugName)
+ .then<void>((_) {}, onError: result.completeError);
+ } on Object {
+ // Sending the computation failed.
+ resultPort.close();
+ rethrow;
+ }
+ return result.future;
+ }
+
/// An [Isolate] object representing the current isolate.
///
/// The current isolate for code using [current]
@@ -345,7 +445,7 @@
/// of the isolate identified by [controlPort],
/// the pause request is ignored by the receiving isolate.
Capability pause([Capability? resumeCapability]) {
- resumeCapability ??= new Capability();
+ resumeCapability ??= Capability();
_pause(resumeCapability);
return resumeCapability;
}
@@ -533,12 +633,12 @@
var listMessage = message as List<Object?>;
var errorDescription = listMessage[0] as String;
var stackDescription = listMessage[1] as String;
- var error = new RemoteError(errorDescription, stackDescription);
+ var error = RemoteError(errorDescription, stackDescription);
controller.addError(error, error.stackTrace);
}
controller.onListen = () {
- RawReceivePort receivePort = new RawReceivePort(handleError);
+ RawReceivePort receivePort = RawReceivePort(handleError);
port = receivePort;
this.addErrorListener(receivePort.sendPort);
};
@@ -765,7 +865,7 @@
final StackTrace stackTrace;
RemoteError(String description, String stackDescription)
: _description = description,
- stackTrace = new StackTrace.fromString(stackDescription);
+ stackTrace = StackTrace.fromString(stackDescription);
String toString() => _description;
}
@@ -795,3 +895,62 @@
/// transferable bytes, even if the calls occur in different isolates.
ByteBuffer materialize();
}
+
+/// Parameter object used by [Isolate.run].
+///
+/// The [_remoteExecute] function is run in a new isolate with a
+/// [_RemoteRunner] object as argument.
+class _RemoteRunner<R> {
+ /// User computation to run.
+ final FutureOr<R> Function() computation;
+
+ /// Port to send isolate computation result on.
+ ///
+ /// Only one object is ever sent on this port.
+ /// If the value is `null`, it is sent by the isolate's "on-exit" handler
+ /// when the isolate terminates without otherwise sending value.
+ /// If the value is a list with one element,
+ /// then it is the result value of the computation.
+ /// Otherwise it is a list with two elements representing an error.
+ /// If the error is sent by the isolate's "on-error" uncaught error handler,
+ /// then the list contains two strings. This also terminates the isolate.
+ /// If sent manually by this class, after capturing the error,
+ /// the list contains one non-`null` [Object] and one [StackTrace].
+ final SendPort resultPort;
+
+ _RemoteRunner(this.computation, this.resultPort);
+
+ /// Run in a new isolate to get the result of [computation].
+ ///
+ /// The result is sent back on [resultPort] as a single-element list.
+ /// A two-element list sent on the same port is an error result.
+ /// When sent by this function, it's always an object and a [StackTrace].
+ /// (The same port listens on uncaught errors from the isolate, which
+ /// sends two-element lists containing [String]s instead).
+ static void _remoteExecute(_RemoteRunner<Object?> runner) {
+ runner._run();
+ }
+
+ void _run() async {
+ R result;
+ try {
+ var potentiallyAsyncResult = computation();
+ if (potentiallyAsyncResult is Future<R>) {
+ result = await potentiallyAsyncResult;
+ } else {
+ result = potentiallyAsyncResult as R;
+ }
+ } catch (e, s) {
+ // If sending fails, the error becomes an uncaught error.
+ Isolate.exit(resultPort, _list2(e, s));
+ }
+ Isolate.exit(resultPort, _list1(result));
+ }
+
+ /// Helper function to create a one-element non-growable list.
+ static List<Object?> _list1(Object? value) => List.filled(1, value);
+
+ /// Helper function to create a two-element non-growable list.
+ static List<Object?> _list2(Object? value1, Object? value2) =>
+ List.filled(2, value1)..[1] = value2;
+}
diff --git a/tests/lib/isolate/isolate_run_test.dart b/tests/lib/isolate/isolate_run_test.dart
new file mode 100644
index 0000000..27312b8
--- /dev/null
+++ b/tests/lib/isolate/isolate_run_test.dart
@@ -0,0 +1,115 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:isolate';
+import 'dart:async';
+import 'package:async_helper/async_helper.dart';
+import 'package:expect/expect.dart';
+
+void main() async {
+ asyncStart();
+ // Sending result back.
+ await testValue();
+ await testAsyncValue();
+ // Sending error from computation back.
+ await testError();
+ await testAsyncError();
+ // Sending uncaught async error back.
+ await testUncaughtError();
+ // Not sending anything back before isolate dies.
+ await testIsolateHangs();
+ await testIsolateKilled();
+ await testIsolateExits();
+ asyncEnd();
+}
+
+final StackTrace stack = StackTrace.fromString("Known Stacktrace");
+final ArgumentError error = ArgumentError.value(42, "name");
+
+var variable = 0;
+
+Future<void> testValue() async {
+ var value = await Isolate.run<int>(() {
+ variable = 1; // Changed in other isolate!
+ Expect.equals(1, variable);
+ return 42;
+ });
+ Expect.equals(42, value);
+ Expect.equals(0, variable);
+}
+
+Future<void> testAsyncValue() async {
+ var value = await Isolate.run<int>(() async {
+ variable = 1;
+ return 42;
+ });
+ Expect.equals(42, value);
+ Expect.equals(0, variable);
+}
+
+Future<void> testError() async {
+ var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() {
+ variable = 1;
+ Error.throwWithStackTrace(error, stack);
+ }));
+ Expect.equals(42, e.invalidValue);
+ Expect.equals("name", e.name);
+ Expect.equals(0, variable);
+}
+
+Future<void> testAsyncError() async {
+ var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() async {
+ variable = 1;
+ Error.throwWithStackTrace(error, stack);
+ }));
+ Expect.equals(42, e.invalidValue);
+ Expect.equals("name", e.name);
+ Expect.equals(0, variable);
+}
+
+Future<void> testUncaughtError() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ unawaited(Future.error(error, stack)); // Uncaught error
+ await Completer().future; // Never completes.
+ return -1;
+ }));
+
+ Expect.type<RemoteError>(e);
+ Expect.equals(error.toString(), e.toString());
+ Expect.equals(0, variable);
+}
+
+Future<void> testIsolateHangs() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ await Completer<Never>().future; // Never completes.
+ // Isolate should end while hanging here, because its event loop is empty.
+ }));
+ Expect.type<RemoteError>(e);
+ Expect.equals("Computation ended without result", e.toString());
+ Expect.equals(0, variable);
+}
+
+Future<void> testIsolateKilled() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ Isolate.current.kill(); // Send kill request.
+ await Completer<Never>().future; // Never completes.
+ // Isolate should get killed while hanging here.
+ }));
+ Expect.type<RemoteError>(e);
+ Expect.equals("Computation ended without result", e.toString());
+ Expect.equals(0, variable);
+}
+
+Future<void> testIsolateExits() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ Isolate.exit(); // Dies here without sending anything back.
+ }));
+ Expect.type<RemoteError>(e);
+ Expect.equals("Computation ended without result", e.toString());
+ Expect.equals(0, variable);
+}
diff --git a/tests/lib_2/isolate/isolate_run_test.dart b/tests/lib_2/isolate/isolate_run_test.dart
new file mode 100644
index 0000000..d39c83d
--- /dev/null
+++ b/tests/lib_2/isolate/isolate_run_test.dart
@@ -0,0 +1,117 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart = 2.9
+
+import 'dart:isolate';
+import 'dart:async';
+import 'package:async_helper/async_helper.dart';
+import 'package:expect/expect.dart';
+
+void main() async {
+ asyncStart();
+ // Sending result back.
+ await testValue();
+ await testAsyncValue();
+ // Sending error from computation back.
+ await testError();
+ await testAsyncError();
+ // Sending uncaught async error back.
+ await testUncaughtError();
+ // Not sending anything back before isolate dies.
+ await testIsolateHangs();
+ await testIsolateKilled();
+ await testIsolateExits();
+ asyncEnd();
+}
+
+final StackTrace stack = StackTrace.fromString("Known Stacktrace");
+final ArgumentError error = ArgumentError.value(42, "name");
+
+var variable = 0;
+
+Future<void> testValue() async {
+ var value = await Isolate.run<int>(() {
+ variable = 1; // Changed in other isolate!
+ Expect.equals(1, variable);
+ return 42;
+ });
+ Expect.equals(42, value);
+ Expect.equals(0, variable);
+}
+
+Future<void> testAsyncValue() async {
+ var value = await Isolate.run<int>(() async {
+ variable = 1;
+ return 42;
+ });
+ Expect.equals(42, value);
+ Expect.equals(0, variable);
+}
+
+Future<void> testError() async {
+ var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() {
+ variable = 1;
+ Error.throwWithStackTrace(error, stack);
+ }));
+ Expect.equals(42, e.invalidValue);
+ Expect.equals("name", e.name);
+ Expect.equals(0, variable);
+}
+
+Future<void> testAsyncError() async {
+ var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() async {
+ variable = 1;
+ Error.throwWithStackTrace(error, stack);
+ }));
+ Expect.equals(42, e.invalidValue);
+ Expect.equals("name", e.name);
+ Expect.equals(0, variable);
+}
+
+Future<void> testUncaughtError() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ unawaited(Future.error(error, stack)); // Uncaught error
+ await Completer().future; // Never completes.
+ return -1;
+ }));
+
+ Expect.type<RemoteError>(e);
+ Expect.equals(error.toString(), e.toString());
+ Expect.equals(0, variable);
+}
+
+Future<void> testIsolateHangs() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ await Completer<Never>().future; // Never completes.
+ // Isolate should end while hanging here, because its event loop is empty.
+ }));
+ Expect.type<RemoteError>(e);
+ Expect.equals("Computation ended without result", e.toString());
+ Expect.equals(0, variable);
+}
+
+Future<void> testIsolateKilled() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ Isolate.current.kill(); // Send kill request.
+ await Completer<Never>().future; // Never completes.
+ // Isolate should get killed while hanging here.
+ }));
+ Expect.type<RemoteError>(e);
+ Expect.equals("Computation ended without result", e.toString());
+ Expect.equals(0, variable);
+}
+
+Future<void> testIsolateExits() async {
+ var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
+ variable = 1;
+ Isolate.exit(); // Dies here without sending anything back.
+ }));
+ Expect.type<RemoteError>(e);
+ Expect.equals("Computation ended without result", e.toString());
+ Expect.equals(0, variable);
+}