Skip to content

Commit 05322f2

Browse files
lrhnCommit Bot
authored and
Commit Bot
committed
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>
1 parent c8950a9 commit 05322f2

File tree

4 files changed

+399
-4
lines changed

4 files changed

+399
-4
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
- Deprecate `SecureSocket.renegotiate` and `RawSecureSocket.renegotiate`,
6464
which were no-ops.
6565

66+
#### `dart:isolate`
67+
68+
- Add `Isolate.run` to run a function in a new isolate.
69+
6670
### Tools
6771

6872
#### Dart command line

sdk/lib/isolate/isolate.dart

+163-4
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,106 @@ class Isolate {
144144
/// inspect the isolate and see uncaught errors or when it terminates.
145145
Isolate(this.controlPort, {this.pauseCapability, this.terminateCapability});
146146

147+
/// Runs [computation] in a new isolate and returns the result.
148+
///
149+
/// ```dart
150+
/// int slowFib(int n) =>
151+
/// n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
152+
///
153+
/// // Compute without blocking current isolate.
154+
/// var fib40 = await Isolate.run(() => slowFib(40));
155+
/// ```
156+
///
157+
/// If [computation] is asynchronous (returns a `Future<R>`) then
158+
/// that future is awaited in the new isolate, completing the entire
159+
/// asynchronous computation, before returning the result.
160+
///
161+
/// ```dart
162+
/// int slowFib(int n) =>
163+
/// n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
164+
/// Stream<int> fibStream() async* {
165+
/// for (var i = 0;; i++) yield slowFib(i);
166+
/// }
167+
///
168+
/// // Returns `Future<int>`.
169+
/// var fib40 = await Isolate.run(() => fibStream().elementAt(40));
170+
/// ```
171+
///
172+
/// If [computation] throws, the isolate is terminated and this
173+
/// function throws the same error.
174+
///
175+
/// ```dart import:convert
176+
/// Future<int> eventualError() async {
177+
/// await Future.delayed(const Duration(seconds: 1));
178+
/// throw StateError("In a bad state!");
179+
/// }
180+
///
181+
/// try {
182+
/// await Isolate.run(eventualError);
183+
/// } on StateError catch (e, s) {
184+
/// print(e.message); // In a bad state!
185+
/// print(LineSplitter.split("$s").first); // Contains "eventualError"
186+
/// }
187+
/// ```
188+
/// Any uncaught asynchronous errors will terminate the computation as well,
189+
/// but will be reported as a [RemoteError] because [addErrorListener]
190+
/// does not provide the original error object.
191+
///
192+
/// The result is sent using [exit], which means it's sent to this
193+
/// isolate without copying.
194+
///
195+
/// The [computation] function and its result (or error) must be
196+
/// sendable between isolates.
197+
///
198+
/// The [debugName] is only used to name the new isolate for debugging.
199+
@Since("2.17")
200+
static Future<R> run<R>(FutureOr<R> computation(), {String? debugName}) {
201+
var result = Completer<R>();
202+
var resultPort = RawReceivePort();
203+
resultPort.handler = (response) {
204+
resultPort.close();
205+
if (response == null) {
206+
// onExit handler message, isolate terminated without sending result.
207+
result.completeError(
208+
RemoteError("Computation ended without result", ""),
209+
StackTrace.empty);
210+
return;
211+
}
212+
var list = response as List<Object?>;
213+
if (list.length == 2) {
214+
var remoteError = list[0];
215+
var remoteStack = list[1];
216+
if (remoteStack is StackTrace) {
217+
// Typed error.
218+
result.completeError(remoteError!, remoteStack);
219+
} else {
220+
// onError handler message, uncaught async error.
221+
// Both values are strings, so calling `toString` is efficient.
222+
var error =
223+
RemoteError(remoteError.toString(), remoteStack.toString());
224+
result.completeError(error, error.stackTrace);
225+
}
226+
} else {
227+
assert(list.length == 1);
228+
result.complete(list[0] as R);
229+
}
230+
};
231+
try {
232+
Isolate.spawn(_RemoteRunner._remoteExecute,
233+
_RemoteRunner<R>(computation, resultPort.sendPort),
234+
onError: resultPort.sendPort,
235+
onExit: resultPort.sendPort,
236+
errorsAreFatal: true,
237+
debugName: debugName)
238+
.then<void>((_) {}, onError: result.completeError);
239+
} on Object {
240+
// Sending the computation failed.
241+
resultPort.close();
242+
rethrow;
243+
}
244+
return result.future;
245+
}
246+
147247
/// An [Isolate] object representing the current isolate.
148248
///
149249
/// The current isolate for code using [current]
@@ -345,7 +445,7 @@ class Isolate {
345445
/// of the isolate identified by [controlPort],
346446
/// the pause request is ignored by the receiving isolate.
347447
Capability pause([Capability? resumeCapability]) {
348-
resumeCapability ??= new Capability();
448+
resumeCapability ??= Capability();
349449
_pause(resumeCapability);
350450
return resumeCapability;
351451
}
@@ -533,12 +633,12 @@ class Isolate {
533633
var listMessage = message as List<Object?>;
534634
var errorDescription = listMessage[0] as String;
535635
var stackDescription = listMessage[1] as String;
536-
var error = new RemoteError(errorDescription, stackDescription);
636+
var error = RemoteError(errorDescription, stackDescription);
537637
controller.addError(error, error.stackTrace);
538638
}
539639

540640
controller.onListen = () {
541-
RawReceivePort receivePort = new RawReceivePort(handleError);
641+
RawReceivePort receivePort = RawReceivePort(handleError);
542642
port = receivePort;
543643
this.addErrorListener(receivePort.sendPort);
544644
};
@@ -765,7 +865,7 @@ class RemoteError implements Error {
765865
final StackTrace stackTrace;
766866
RemoteError(String description, String stackDescription)
767867
: _description = description,
768-
stackTrace = new StackTrace.fromString(stackDescription);
868+
stackTrace = StackTrace.fromString(stackDescription);
769869
String toString() => _description;
770870
}
771871

@@ -795,3 +895,62 @@ abstract class TransferableTypedData {
795895
/// transferable bytes, even if the calls occur in different isolates.
796896
ByteBuffer materialize();
797897
}
898+
899+
/// Parameter object used by [Isolate.run].
900+
///
901+
/// The [_remoteExecute] function is run in a new isolate with a
902+
/// [_RemoteRunner] object as argument.
903+
class _RemoteRunner<R> {
904+
/// User computation to run.
905+
final FutureOr<R> Function() computation;
906+
907+
/// Port to send isolate computation result on.
908+
///
909+
/// Only one object is ever sent on this port.
910+
/// If the value is `null`, it is sent by the isolate's "on-exit" handler
911+
/// when the isolate terminates without otherwise sending value.
912+
/// If the value is a list with one element,
913+
/// then it is the result value of the computation.
914+
/// Otherwise it is a list with two elements representing an error.
915+
/// If the error is sent by the isolate's "on-error" uncaught error handler,
916+
/// then the list contains two strings. This also terminates the isolate.
917+
/// If sent manually by this class, after capturing the error,
918+
/// the list contains one non-`null` [Object] and one [StackTrace].
919+
final SendPort resultPort;
920+
921+
_RemoteRunner(this.computation, this.resultPort);
922+
923+
/// Run in a new isolate to get the result of [computation].
924+
///
925+
/// The result is sent back on [resultPort] as a single-element list.
926+
/// A two-element list sent on the same port is an error result.
927+
/// When sent by this function, it's always an object and a [StackTrace].
928+
/// (The same port listens on uncaught errors from the isolate, which
929+
/// sends two-element lists containing [String]s instead).
930+
static void _remoteExecute(_RemoteRunner<Object?> runner) {
931+
runner._run();
932+
}
933+
934+
void _run() async {
935+
R result;
936+
try {
937+
var potentiallyAsyncResult = computation();
938+
if (potentiallyAsyncResult is Future<R>) {
939+
result = await potentiallyAsyncResult;
940+
} else {
941+
result = potentiallyAsyncResult as R;
942+
}
943+
} catch (e, s) {
944+
// If sending fails, the error becomes an uncaught error.
945+
Isolate.exit(resultPort, _list2(e, s));
946+
}
947+
Isolate.exit(resultPort, _list1(result));
948+
}
949+
950+
/// Helper function to create a one-element non-growable list.
951+
static List<Object?> _list1(Object? value) => List.filled(1, value);
952+
953+
/// Helper function to create a two-element non-growable list.
954+
static List<Object?> _list2(Object? value1, Object? value2) =>
955+
List.filled(2, value1)..[1] = value2;
956+
}
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:isolate';
6+
import 'dart:async';
7+
import 'package:async_helper/async_helper.dart';
8+
import 'package:expect/expect.dart';
9+
10+
void main() async {
11+
asyncStart();
12+
// Sending result back.
13+
await testValue();
14+
await testAsyncValue();
15+
// Sending error from computation back.
16+
await testError();
17+
await testAsyncError();
18+
// Sending uncaught async error back.
19+
await testUncaughtError();
20+
// Not sending anything back before isolate dies.
21+
await testIsolateHangs();
22+
await testIsolateKilled();
23+
await testIsolateExits();
24+
asyncEnd();
25+
}
26+
27+
final StackTrace stack = StackTrace.fromString("Known Stacktrace");
28+
final ArgumentError error = ArgumentError.value(42, "name");
29+
30+
var variable = 0;
31+
32+
Future<void> testValue() async {
33+
var value = await Isolate.run<int>(() {
34+
variable = 1; // Changed in other isolate!
35+
Expect.equals(1, variable);
36+
return 42;
37+
});
38+
Expect.equals(42, value);
39+
Expect.equals(0, variable);
40+
}
41+
42+
Future<void> testAsyncValue() async {
43+
var value = await Isolate.run<int>(() async {
44+
variable = 1;
45+
return 42;
46+
});
47+
Expect.equals(42, value);
48+
Expect.equals(0, variable);
49+
}
50+
51+
Future<void> testError() async {
52+
var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() {
53+
variable = 1;
54+
Error.throwWithStackTrace(error, stack);
55+
}));
56+
Expect.equals(42, e.invalidValue);
57+
Expect.equals("name", e.name);
58+
Expect.equals(0, variable);
59+
}
60+
61+
Future<void> testAsyncError() async {
62+
var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() async {
63+
variable = 1;
64+
Error.throwWithStackTrace(error, stack);
65+
}));
66+
Expect.equals(42, e.invalidValue);
67+
Expect.equals("name", e.name);
68+
Expect.equals(0, variable);
69+
}
70+
71+
Future<void> testUncaughtError() async {
72+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
73+
variable = 1;
74+
unawaited(Future.error(error, stack)); // Uncaught error
75+
await Completer().future; // Never completes.
76+
return -1;
77+
}));
78+
79+
Expect.type<RemoteError>(e);
80+
Expect.equals(error.toString(), e.toString());
81+
Expect.equals(0, variable);
82+
}
83+
84+
Future<void> testIsolateHangs() async {
85+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
86+
variable = 1;
87+
await Completer<Never>().future; // Never completes.
88+
// Isolate should end while hanging here, because its event loop is empty.
89+
}));
90+
Expect.type<RemoteError>(e);
91+
Expect.equals("Computation ended without result", e.toString());
92+
Expect.equals(0, variable);
93+
}
94+
95+
Future<void> testIsolateKilled() async {
96+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
97+
variable = 1;
98+
Isolate.current.kill(); // Send kill request.
99+
await Completer<Never>().future; // Never completes.
100+
// Isolate should get killed while hanging here.
101+
}));
102+
Expect.type<RemoteError>(e);
103+
Expect.equals("Computation ended without result", e.toString());
104+
Expect.equals(0, variable);
105+
}
106+
107+
Future<void> testIsolateExits() async {
108+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
109+
variable = 1;
110+
Isolate.exit(); // Dies here without sending anything back.
111+
}));
112+
Expect.type<RemoteError>(e);
113+
Expect.equals("Computation ended without result", e.toString());
114+
Expect.equals(0, variable);
115+
}

0 commit comments

Comments
 (0)