diff --git a/CHANGELOG.md b/CHANGELOG.md index 70414c2b61f..a5313a29ec8 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 48c046f11e4..517beee43b4 100644 --- a/sdk/lib/isolate/isolate.dart +++ b/sdk/lib/isolate/isolate.dart @@ -144,6 +144,106 @@ class Isolate { /// 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`) 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 fibStream() async* { + /// for (var i = 0;; i++) yield slowFib(i); + /// } + /// + /// // Returns `Future`. + /// 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 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 run(FutureOr computation(), {String? debugName}) { + var result = Completer(); + 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; + 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(computation, resultPort.sendPort), + onError: resultPort.sendPort, + onExit: resultPort.sendPort, + errorsAreFatal: true, + debugName: debugName) + .then((_) {}, 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 @@ class Isolate { /// 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 @@ class Isolate { var listMessage = message as List; 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 @@ class RemoteError implements Error { 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 @@ abstract class TransferableTypedData { /// 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 { + /// User computation to run. + final FutureOr 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 runner) { + runner._run(); + } + + void _run() async { + R result; + try { + var potentiallyAsyncResult = computation(); + if (potentiallyAsyncResult is Future) { + 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 _list1(Object? value) => List.filled(1, value); + + /// Helper function to create a two-element non-growable list. + static List _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 00000000000..27312b8a951 --- /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 testValue() async { + var value = await Isolate.run(() { + variable = 1; // Changed in other isolate! + Expect.equals(1, variable); + return 42; + }); + Expect.equals(42, value); + Expect.equals(0, variable); +} + +Future testAsyncValue() async { + var value = await Isolate.run(() async { + variable = 1; + return 42; + }); + Expect.equals(42, value); + Expect.equals(0, variable); +} + +Future testError() async { + var e = await asyncExpectThrows(Isolate.run(() { + variable = 1; + Error.throwWithStackTrace(error, stack); + })); + Expect.equals(42, e.invalidValue); + Expect.equals("name", e.name); + Expect.equals(0, variable); +} + +Future testAsyncError() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + Error.throwWithStackTrace(error, stack); + })); + Expect.equals(42, e.invalidValue); + Expect.equals("name", e.name); + Expect.equals(0, variable); +} + +Future testUncaughtError() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + unawaited(Future.error(error, stack)); // Uncaught error + await Completer().future; // Never completes. + return -1; + })); + + Expect.type(e); + Expect.equals(error.toString(), e.toString()); + Expect.equals(0, variable); +} + +Future testIsolateHangs() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + await Completer().future; // Never completes. + // Isolate should end while hanging here, because its event loop is empty. + })); + Expect.type(e); + Expect.equals("Computation ended without result", e.toString()); + Expect.equals(0, variable); +} + +Future testIsolateKilled() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + Isolate.current.kill(); // Send kill request. + await Completer().future; // Never completes. + // Isolate should get killed while hanging here. + })); + Expect.type(e); + Expect.equals("Computation ended without result", e.toString()); + Expect.equals(0, variable); +} + +Future testIsolateExits() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + Isolate.exit(); // Dies here without sending anything back. + })); + Expect.type(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 00000000000..d39c83d48c9 --- /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 testValue() async { + var value = await Isolate.run(() { + variable = 1; // Changed in other isolate! + Expect.equals(1, variable); + return 42; + }); + Expect.equals(42, value); + Expect.equals(0, variable); +} + +Future testAsyncValue() async { + var value = await Isolate.run(() async { + variable = 1; + return 42; + }); + Expect.equals(42, value); + Expect.equals(0, variable); +} + +Future testError() async { + var e = await asyncExpectThrows(Isolate.run(() { + variable = 1; + Error.throwWithStackTrace(error, stack); + })); + Expect.equals(42, e.invalidValue); + Expect.equals("name", e.name); + Expect.equals(0, variable); +} + +Future testAsyncError() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + Error.throwWithStackTrace(error, stack); + })); + Expect.equals(42, e.invalidValue); + Expect.equals("name", e.name); + Expect.equals(0, variable); +} + +Future testUncaughtError() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + unawaited(Future.error(error, stack)); // Uncaught error + await Completer().future; // Never completes. + return -1; + })); + + Expect.type(e); + Expect.equals(error.toString(), e.toString()); + Expect.equals(0, variable); +} + +Future testIsolateHangs() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + await Completer().future; // Never completes. + // Isolate should end while hanging here, because its event loop is empty. + })); + Expect.type(e); + Expect.equals("Computation ended without result", e.toString()); + Expect.equals(0, variable); +} + +Future testIsolateKilled() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + Isolate.current.kill(); // Send kill request. + await Completer().future; // Never completes. + // Isolate should get killed while hanging here. + })); + Expect.type(e); + Expect.equals("Computation ended without result", e.toString()); + Expect.equals(0, variable); +} + +Future testIsolateExits() async { + var e = await asyncExpectThrows(Isolate.run(() async { + variable = 1; + Isolate.exit(); // Dies here without sending anything back. + })); + Expect.type(e); + Expect.equals("Computation ended without result", e.toString()); + Expect.equals(0, variable); +}