Skip to content

Commit a5f25ee

Browse files
lrhnCommit Bot
authored and
Commit Bot
committed
Add Isolate.run.
Adds static method on `Isolate` to run an asynchronous function in a separate isolate. Change-Id: I673373fa02524f1d0b88099027cfaf1b796eb344 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/254960 Commit-Queue: Lasse Nielsen <[email protected]> Reviewed-by: Martin Kustermann <[email protected]>
1 parent 9299aa2 commit a5f25ee

File tree

4 files changed

+424
-2
lines changed

4 files changed

+424
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@
4343
to be deleted in a future release. See the related breaking change
4444
request [#49536](https://github.com/dart-lang/sdk/issues/49536).
4545

46+
#### `dart:isolate`
47+
48+
- Add `Isolate.run` to run a function in a new isolate.
49+
4650
### Tools
4751

4852
#### Linter
4953

5054
Updated the Linter to `1.27.0`, which includes changes that
5155

52-
- fix `avoid_redundant_argument_values` when referencing required
56+
- fix `avoid_redundant_argument_values` when referencing required
5357
parameters in legacy libraries.
5458
- improve performance for `use_late_for_private_fields_and_variables`.
5559
- add new lint: `use_string_in_part_of_directives`.
@@ -105,7 +109,7 @@ them, you must set the lower bound on the SDK constraint for your package to
105109

106110
[language version]: https://dart.dev/guides/language/evolution
107111

108-
- **[Enhanced type inference for generic invocations with function literals][]**:
112+
- **[Enhanced type inference for generic invocations with function literals][]**:
109113
Invocations of generic methods/constructors that supply function literal
110114
arguments now have improved type inference. This primarily affects the
111115
`Iterable.fold` method. For example, in previous versions of Dart, the

sdk/lib/isolate/isolate.dart

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,113 @@ class Isolate {
150150
/// inspect the isolate and see uncaught errors or when it terminates.
151151
Isolate(this.controlPort, {this.pauseCapability, this.terminateCapability});
152152

153+
/// Runs [computation] in a new isolate and returns the result.
154+
///
155+
/// ```dart
156+
/// int slowFib(int n) =>
157+
/// n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
158+
///
159+
/// // Compute without blocking current isolate.
160+
/// var fib40 = await Isolate.run(() => slowFib(40));
161+
/// ```
162+
///
163+
/// If [computation] is asynchronous (returns a `Future<R>`) then
164+
/// that future is awaited in the new isolate, completing the entire
165+
/// asynchronous computation, before returning the result.
166+
///
167+
/// ```dart
168+
/// int slowFib(int n) =>
169+
/// n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
170+
/// Stream<int> fibStream() async* {
171+
/// for (var i = 0;; i++) yield slowFib(i);
172+
/// }
173+
///
174+
/// // Returns `Future<int>`.
175+
/// var fib40 = await Isolate.run(() => fibStream().elementAt(40));
176+
/// ```
177+
///
178+
/// If [computation] throws, the isolate is terminated and this
179+
/// function throws the same error.
180+
///
181+
/// ```dart import:convert
182+
/// Future<int> eventualError() async {
183+
/// await Future.delayed(const Duration(seconds: 1));
184+
/// throw StateError("In a bad state!");
185+
/// }
186+
///
187+
/// try {
188+
/// await Isolate.run(eventualError);
189+
/// } on StateError catch (e, s) {
190+
/// print(e.message); // In a bad state!
191+
/// print(LineSplitter.split("$s").first); // Contains "eventualError"
192+
/// }
193+
/// ```
194+
/// Any uncaught asynchronous errors will terminate the computation as well,
195+
/// but will be reported as a [RemoteError] because [addErrorListener]
196+
/// does not provide the original error object.
197+
///
198+
/// The result is sent using [exit], which means it's sent to this
199+
/// isolate without copying.
200+
///
201+
/// The [computation] function and its result (or error) must be
202+
/// sendable between isolates.
203+
///
204+
/// The [debugName] is only used to name the new isolate for debugging.
205+
@Since("2.19")
206+
static Future<R> run<R>(FutureOr<R> computation(), {String? debugName}) {
207+
var result = Completer<R>();
208+
var resultPort = RawReceivePort();
209+
resultPort.handler = (response) {
210+
resultPort.close();
211+
if (response == null) {
212+
// onExit handler message, isolate terminated without sending result.
213+
result.completeError(
214+
RemoteError("Computation ended without result", ""),
215+
StackTrace.empty);
216+
return;
217+
}
218+
var list = response as List<Object?>;
219+
if (list.length == 2) {
220+
var remoteError = list[0];
221+
var remoteStack = list[1];
222+
if (remoteStack is StackTrace) {
223+
// Typed error.
224+
result.completeError(remoteError!, remoteStack);
225+
} else {
226+
// onError handler message, uncaught async error.
227+
// Both values are strings, so calling `toString` is efficient.
228+
var error =
229+
RemoteError(remoteError.toString(), remoteStack.toString());
230+
result.completeError(error, error.stackTrace);
231+
}
232+
} else {
233+
assert(list.length == 1);
234+
result.complete(list[0] as R);
235+
}
236+
};
237+
try {
238+
Isolate.spawn(_RemoteRunner._remoteExecute,
239+
_RemoteRunner<R>(computation, resultPort.sendPort),
240+
onError: resultPort.sendPort,
241+
onExit: resultPort.sendPort,
242+
errorsAreFatal: true,
243+
debugName: debugName)
244+
.then<void>((_) {}, onError: (error, stack) {
245+
// Sending the computation failed asynchronously.
246+
// Do not expect a response, report the error asynchronously.
247+
resultPort.close();
248+
result.completeError(error, stack);
249+
});
250+
} on Object {
251+
// Sending the computation failed synchronously.
252+
// This is not expected to happen, but if it does,
253+
// the synchronous error is respected and rethrown synchronously.
254+
resultPort.close();
255+
rethrow;
256+
}
257+
return result.future;
258+
}
259+
153260
/// An [Isolate] object representing the current isolate.
154261
///
155262
/// The current isolate for code using [current]
@@ -807,3 +914,62 @@ abstract class TransferableTypedData {
807914
/// transferable bytes, even if the calls occur in different isolates.
808915
ByteBuffer materialize();
809916
}
917+
918+
/// Parameter object used by [Isolate.run].
919+
///
920+
/// The [_remoteExecute] function is run in a new isolate with a
921+
/// [_RemoteRunner] object as argument.
922+
class _RemoteRunner<R> {
923+
/// User computation to run.
924+
final FutureOr<R> Function() computation;
925+
926+
/// Port to send isolate computation result on.
927+
///
928+
/// Only one object is ever sent on this port.
929+
/// If the value is `null`, it is sent by the isolate's "on-exit" handler
930+
/// when the isolate terminates without otherwise sending value.
931+
/// If the value is a list with one element,
932+
/// then it is the result value of the computation.
933+
/// Otherwise it is a list with two elements representing an error.
934+
/// If the error is sent by the isolate's "on-error" uncaught error handler,
935+
/// then the list contains two strings. This also terminates the isolate.
936+
/// If sent manually by this class, after capturing the error,
937+
/// the list contains one non-`null` [Object] and one [StackTrace].
938+
final SendPort resultPort;
939+
940+
_RemoteRunner(this.computation, this.resultPort);
941+
942+
/// Run in a new isolate to get the result of [computation].
943+
///
944+
/// The result is sent back on [resultPort] as a single-element list.
945+
/// A two-element list sent on the same port is an error result.
946+
/// When sent by this function, it's always an object and a [StackTrace].
947+
/// (The same port listens on uncaught errors from the isolate, which
948+
/// sends two-element lists containing [String]s instead).
949+
static void _remoteExecute(_RemoteRunner<Object?> runner) {
950+
runner._run();
951+
}
952+
953+
void _run() async {
954+
R result;
955+
try {
956+
var potentiallyAsyncResult = computation();
957+
if (potentiallyAsyncResult is Future<R>) {
958+
result = await potentiallyAsyncResult;
959+
} else {
960+
result = potentiallyAsyncResult;
961+
}
962+
} catch (e, s) {
963+
// If sending fails, the error becomes an uncaught error.
964+
Isolate.exit(resultPort, _list2(e, s));
965+
}
966+
Isolate.exit(resultPort, _list1(result));
967+
}
968+
969+
/// Helper function to create a one-element non-growable list.
970+
static List<Object?> _list1(Object? value) => List.filled(1, value);
971+
972+
/// Helper function to create a two-element non-growable list.
973+
static List<Object?> _list2(Object? value1, Object? value2) =>
974+
List.filled(2, value1)..[1] = value2;
975+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
// Failing to start.
25+
await testInvalidMessage();
26+
asyncEnd();
27+
}
28+
29+
final StackTrace stack = StackTrace.fromString("Known Stacktrace");
30+
final ArgumentError error = ArgumentError.value(42, "name");
31+
32+
var variable = 0;
33+
34+
Future<void> testValue() async {
35+
var value = await Isolate.run<int>(() {
36+
variable = 1; // Changed in other isolate!
37+
Expect.equals(1, variable);
38+
return 42;
39+
});
40+
Expect.equals(42, value);
41+
Expect.equals(0, variable);
42+
}
43+
44+
Future<void> testAsyncValue() async {
45+
var value = await Isolate.run<int>(() async {
46+
variable = 1;
47+
return 42;
48+
});
49+
Expect.equals(42, value);
50+
Expect.equals(0, variable);
51+
}
52+
53+
Future<void> testError() async {
54+
var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() {
55+
variable = 1;
56+
Error.throwWithStackTrace(error, stack);
57+
}));
58+
Expect.equals(42, e.invalidValue);
59+
Expect.equals("name", e.name);
60+
Expect.equals(0, variable);
61+
}
62+
63+
Future<void> testAsyncError() async {
64+
var e = await asyncExpectThrows<ArgumentError>(Isolate.run<int>(() async {
65+
variable = 1;
66+
Error.throwWithStackTrace(error, stack);
67+
}));
68+
Expect.equals(42, e.invalidValue);
69+
Expect.equals("name", e.name);
70+
Expect.equals(0, variable);
71+
}
72+
73+
Future<void> testUncaughtError() async {
74+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
75+
variable = 1;
76+
unawaited(Future.error(error, stack)); // Uncaught error
77+
await Completer().future; // Never completes.
78+
return -1;
79+
}));
80+
81+
Expect.type<RemoteError>(e);
82+
Expect.equals(error.toString(), e.toString());
83+
Expect.equals(0, variable);
84+
}
85+
86+
Future<void> testIsolateHangs() async {
87+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
88+
variable = 1;
89+
await Completer<Never>().future; // Never completes.
90+
// Isolate should end while hanging here, because its event loop is empty.
91+
}));
92+
Expect.type<RemoteError>(e);
93+
Expect.equals("Computation ended without result", e.toString());
94+
Expect.equals(0, variable);
95+
}
96+
97+
Future<void> testIsolateKilled() async {
98+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
99+
variable = 1;
100+
Isolate.current.kill(); // Send kill request.
101+
await Completer<Never>().future; // Never completes.
102+
// Isolate should get killed while hanging here.
103+
}));
104+
Expect.type<RemoteError>(e);
105+
Expect.equals("Computation ended without result", e.toString());
106+
Expect.equals(0, variable);
107+
}
108+
109+
Future<void> testIsolateExits() async {
110+
var e = await asyncExpectThrows<RemoteError>(Isolate.run<int>(() async {
111+
variable = 1;
112+
Isolate.exit(); // Dies here without sending anything back.
113+
}));
114+
Expect.type<RemoteError>(e);
115+
Expect.equals("Computation ended without result", e.toString());
116+
Expect.equals(0, variable);
117+
}
118+
119+
Future<void> testInvalidMessage() async {
120+
// Regression test for http://dartbug.com/48516
121+
var unsendable = RawReceivePort();
122+
await asyncExpectThrows<Error>(Isolate.run<void>(() => unsendable));
123+
unsendable.close();
124+
// Test should not hang.
125+
}

0 commit comments

Comments
 (0)