Skip to content

Commit f1776bb

Browse files
authored
Update shared memory proposal (#4620)
* Restrict to non-late variables in shared closures and deeply immutable classes * Add event loop and ownership APIs to Isolate
1 parent 171482c commit f1776bb

2 files changed

Lines changed: 167 additions & 69 deletions

File tree

working/333 - shared memory multithreading/proposal.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,9 +1111,6 @@ abstract interface class Coroutine {
11111111
external static Coroutine create(void Function() body);
11121112
11131113
/// Suspends the given currently running coroutine.
1114-
///
1115-
/// This makes `resume` return with
1116-
/// Expects resumer to pass back a value of type [R].
11171114
external static void suspend();
11181115
11191116
/// Resumes previously suspended coroutine.

working/333 - shared memory multithreading/shared_native_memory.md

Lines changed: 167 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ Porting this code to Dart using `dart:ffi` is currently impossible, as FFI only
2626
supports two specific callback types:
2727
2828
- [`NativeCallable.isolateLocal`][native-callable-isolate-local]: native caller
29-
must have an exclusive access to an isolate in which callback was created.
30-
This type of callback works if Dart calls C and C calls back into Dart
31-
synchronously. It also works if caller uses VM C API for entering isolates
29+
must have an exclusive access to an isolate in which callback was created - or
30+
isolate must be pinned (owned) by a thread which invokes the callback. In the
31+
later case FFI trampoline will take care of entering the target isolate even
32+
if needed. This type of callback works if Dart calls C and C calls back into
33+
Dart synchronously. It also works if caller uses VM C API for entering isolates
3234
(e.g.`Dart_EnterIsolate`/`Dart_ExitIsolate`).
3335
- [`NativeCallable.listener`][native-callable-listener]: native caller
3436
effectively sends a message to the isolate which created the callback and does
@@ -167,59 +169,65 @@ classes are also deeply immutable: `SendPort`, `Capability`, `RegExp`,
167169
168170
All compile time constants are deeply immutable instances.
169171
170-
`TypedData` and `Struct` instances are deeply immutable when backed by native
171-
(external) memory.
172+
`TypedData` and `Struct` instances are considered deeply immutable.
173+
174+
> [!IMPORTANT]
175+
>
176+
> There is a consideration here that not all `TypedData` instances can be
177+
> shared on the Web, where there is separation between `ArrayBuffer` and
178+
> `SharedArrayBuffer` exists.
179+
180+
[deeply immutable]: https://github.com/dart-lang/sdk/blob/bb59b5c72c52369e1b0d21940008c4be7e6d43b3/runtime/docs/deeply_immutable.md
181+
172182
173183
Unmodifiable lists (`List.unmodifiable`) which contain deeply immutable
174184
instances are deeply immutable.
175185
176-
Closures which capture only `final` variables containing deeply immutable
177-
instances are deeply immutable.
186+
Closures which capture only `final`, non-`late` variables containing deeply
187+
immutable instances are deeply immutable.
178188
179-
Finally, instances of classes annotated with `@pragma('vm:deeply-immutable')`
180-
are deeply immutable. It is a compile error if classes annotated with this
181-
pragma contain non-`final` fields. It is an compile time error if static
182-
type of field within annotated class excludes deeply immutable instances.
183-
If the static type of a field in a deeply immutable class is not
184-
deeply immutable type - then compiler must insert checks in the constructor to
185-
guarantee that this field is initialized to a deeply immutable value.
189+
Finally, instances of classes _marked as deeply immutable_ by being annotated
190+
with `@pragma('vm:deeply-immutable')` are deeply immutable. For any class
191+
which is marked as deeply immutable it is a compile time error if:
186192
187-
> [!IMPORTANT]
188-
>
189-
> **TODO** should we allow sharing of all `TypedData` (and by extension
190-
> `Struct`) objects? This seems very convenient. There is a consideration
191-
> here that not all `TypedData` instances can be shared on the Web, where
192-
> there is separation between `ArrayBuffer` and `SharedArrayBuffer` exists.
193+
* a subclass of such class which is not itself marked as deeply immutable.
194+
* a superclass of such class is not `Object` or a class itself marked as
195+
deeply immutable.
196+
* such class contains contains non-`final` or `late final` instance variables.
193197
194-
[deeply immutable]: https://github.com/dart-lang/sdk/blob/bb59b5c72c52369e1b0d21940008c4be7e6d43b3/runtime/docs/deeply_immutable.md
198+
Compiler must ensure that instance variables in deeply immutable instances
199+
are initialized with deeply immutable values. If this can't be guarateed
200+
statically then compiler must insert appropriate checks into the constructor
201+
to guarantee this invariant.
195202
196-
### Shared fields and variables (`@pragma('vm:shared')`).
203+
### Shared variables (`@pragma('vm:shared')`).
197204
198-
Static fields and global variables annotated with `@pragma('vm:shared')` are
205+
Static and global variables annotated with `@pragma('vm:shared')` are
199206
shared across all isolates in the isolate group.
200207
201-
A field or variable annotated with `@pragma('vm:shared')` can only contain
202-
values which are deeply immutable objects.
208+
A variable annotated with `@pragma('vm:shared')` can only contain values
209+
which are deeply immutable objects.
203210
204-
* It is a compile time error to annotate a field or variable the static type of
211+
* It is a compile time error to annotate a variable the static type of
205212
which excludes deeply immutable objects;
206-
* If static type of a field is a super-type for both deeply immutable and
213+
* If static type of a variable is a super-type for both deeply immutable and
207214
non-deeply immutable objects then compiler will insert a runtime check
208-
which ensures that values assigned to such field are deeply immutable.
209-
* A field or variable annotated with `@pragma('vm:shared')` must be `final`.
215+
which ensures that values assigned to such variable are deeply immutable.
216+
* A variable annotated with `@pragma('vm:shared')` must be `final` and
217+
non-`late`.
210218
211219
> [!NOTE]
212220
>
213-
> Restrictions imposed above are the same as ones imposed on field in deeply
214-
> immutable classes.
221+
> Restrictions imposed above are the same as ones imposed on instance
222+
> variables in deeply immutable classes.
215223
216-
Shared fields must guarantee atomic initialization: if multiple threads
217-
access the same uninitialized field then only one thread will invoke the
218-
initializer and initialize the field, all other threads will block until
219-
initialization is complete.
224+
Shared static and global variables must guarantee atomic initialization: if
225+
multiple threads access the same uninitialized variable then only one
226+
thread will invoke the initializer and initialize the variable, all other
227+
threads will block until initialization is complete.
220228
221229
Outside of initialization we however do **not** require strong (e.g.
222-
sequentially consistent) atomicity when reading or writing shared fields.
230+
sequentially consistent) atomicity when reading or writing shared variables.
223231
We only require that no thread can ever observe a partially initialized Dart
224232
object. See [Memory Model](#memory-model) for more details.
225233
@@ -230,7 +238,7 @@ Today Dart runtime always executes Dart code within a specific isolate.
230238
within specific _isolate group_ but outside of a specific isolate. When Dart
231239
code is executed in such a way it can only access static state which is shared
232240
between isolates (`@pragma('vm:shared')`) and attempts to access isolated state
233-
will cause `FieldAccessError` to be thrown.
241+
will cause `AccessError` to be thrown.
234242
235243
```dart
236244
/// Constructs a [NativeCallable] that can be invoked from any thread.
@@ -239,8 +247,8 @@ will cause `FieldAccessError` to be thrown.
239247
/// the [callback] will be executed within the isolate group
240248
/// of the [Isolate] which originally constructed the callable.
241249
/// Specifically, this means that an attempt to access any
242-
/// static or global field which is not shared between
243-
/// isolates in a group will result in a [FieldAccessError].
250+
/// static or global variable which is not shared between
251+
/// isolates in a group will result in a [AccessError].
244252
///
245253
/// If an exception is thrown by the [callback], the
246254
/// native function will return the `exceptionalReturn`,
@@ -305,22 +313,115 @@ class Isolate {
305313
/// Throws [TimeoutException] if [timeout] has been reached while waiting
306314
/// to acquire exclusive access to the isolate.
307315
///
308-
/// Throws [StateError] if target isolate is owned by another thread and
309-
/// thus can't be entered from a different thread.
316+
/// Throws an error if target isolate is pinned to another thread and
317+
/// thus can't be entered from this threadn. See [pinToCurrentThread] and
318+
/// [isPinnedToCurrentThread].
319+
///
320+
/// Throws an error if the target isolate belongs to another
321+
/// isolate group.
322+
///
323+
/// Throws an error if [f] is not deeply immutable.
324+
///
325+
/// Throws an error if result returned by [f] is not deeply immutable.
326+
external R runSync<R>(R Function() f, {Duration? timeout});
327+
328+
/// Create a new isolate in the current isolate group.
329+
///
330+
/// Similar to `Dart_CreateIsolateInGroup` Dart VM C API.
331+
///
332+
/// The isolate has been created, but its event loop is not running.
333+
///
334+
/// To start processing isolate's messages:
335+
///
336+
/// * start isolate's event loop synchronously on the current thread
337+
/// by calling [Isolate.runEventLoopSync]
338+
/// * integrate isolate's event loop with an external event loop by
339+
/// registering event callback ([Isolate.onEvent]) to forward
340+
/// event notifications to an external event loop and then draining
341+
/// pending events ([Isolate.handleEvent]) from that event loop.
342+
external static Isolate create({String? debugName});
343+
344+
/// Shut down target isolate.
345+
///
346+
/// Shutting down the isolate stops its event loop without processing
347+
/// any pending messages and closes all open receive ports owned by the
348+
/// isolate.
349+
///
350+
/// This function will block until it acquires exclusive access to the
351+
/// target isolate. Isolate can only be entered for synchronous execution
352+
/// between turns of its event loop, when no other thread is
353+
/// executing code in the target isolate.
354+
external void shutDown();
355+
356+
/// Pin current isolate to the current OS thread.
357+
///
358+
/// Once an isolate is pinned to an OS thread it cannot be
359+
/// entered by any other OS thread. An attempt to acquire
360+
/// exclusive access to it from another thread will fail with
361+
/// an error.
310362
///
311-
/// Throws [ArgumentError] if [f] is not deeply immutable.
363+
/// Equivalent to `Dart_SetCurrentThreadOwnsIsolate` Dart VM C API.
312364
///
313-
/// Throws [StateError] if result returned by [f] is not deeply immutable.
314-
R runSync<R>(R Function() f, {Duration? timeout});
365+
/// Returns `true` on success and `false` otherwise (e.g. if target isolate
366+
/// is already pinned to another thread).
367+
external static bool pinToThread();
368+
369+
/// Whether the isolate is pinned to the current OS thread.
370+
///
371+
/// Equivalent to `Dart_GetCurrentThreadOwnsIsolate` Dart VM C API.
372+
external bool get isPinnedToCurrentThread;
373+
374+
/// Run event loop for the target isolate synchronously on the current thread.
375+
///
376+
/// This function will block until it acquires exclusive access to the
377+
/// target isolate. Isolate can only be entered for synchronous execution
378+
/// between turns of its event loop, when no other thread is
379+
/// executing code in the target isolate.
380+
///
381+
/// This function will return once the isolate has no open keep-alive
382+
/// receive ports.
383+
///
384+
/// The isolate will be marked as pinned to the current thread.
385+
///
386+
/// Similar to `Dart_RunLoop` Dart VM C API, but unlike `Dart_RunLoop` this
387+
/// function executes isolate's event loop on the current thread instead
388+
/// of delegating it into the thread-pool.
389+
///
390+
/// Throws an error if target isolate is pinned to another thread or already
391+
/// has an event loop running.
392+
external void runEventLoopSync();
393+
394+
/// Event notify callback for the isolate.
395+
///
396+
/// Provided callback will be called once for every new event which isolate
397+
/// needs to react to. Pending events can be then later be drained
398+
/// by calling [Isolate.handleEvent].
399+
///
400+
/// Provided [callback] must be deeply immutable and will be called
401+
/// on an arbitrary thread and not necessarily within any isolate. See
402+
/// [NativeCallable.isolateGroupBound].
403+
///
404+
/// IMPORTANT: [Isolate.handleEvent] *MUST NOT* be called from the
405+
/// `callback` as this will cause a dead-locks of the Dart execution
406+
/// environment.
407+
///
408+
/// Similar to `Dart_SetMessageNotifyCallback` Dart VM C API.
409+
external void set onEvent(void Function(Isolate) callback);
410+
411+
/// Handle at most one pending event for the isolate.
412+
///
413+
/// This function does nothing if there are no pending events.
414+
///
415+
/// This function will block until it acquires exclusive access to the
416+
/// target isolate. Isolate can only be entered for synchronous execution
417+
/// between turns of its event loop, when no other thread is
418+
/// executing code in the target isolate.
419+
///
420+
/// Similar to `Dart_HandleMessage` Dart VM C API.
421+
external void handleEvent();
315422
}
316423
```
317424

318-
**TODO**: Furthermore we might want to facilitate integration with third-party
319-
event-loops: e.g. allow to create isolate without scheduling its event loop on
320-
our own thread pool and provide equivalents of `Dart_SetMessageNotifyCallback`
321-
and `Dart_HandleMessage`. Though maybe we should not bundle this all together
322-
into one update.
323-
324425
### Scoped thread local values
325426

326427
```dart
@@ -339,11 +440,11 @@ final class ScopedThreadLocal<T> {
339440
/// If this [ScopedThreadLocal] was uninitialized then it will be reset to this state
340441
/// when execution of [f] completes.
341442
///
342-
/// Throws [StateError] if this [ScopedThreadLocal] does not have an initializer.
443+
/// Throws an error if this [ScopedThreadLocal] does not have an initializer.
343444
external void runInitialized<R>(R Function(T) f);
344445
345446
/// Returns the value specified by the closest enclosing invocation of [with] or
346-
/// throws [StateError] if this [ScopedThreadLocal] is not bound to a value.
447+
/// throws an error if this [ScopedThreadLocal] is not bound to a value.
347448
external T get value;
348449
349450
/// Returns whether this [ScopedThreadLocal] is bound to a value.
@@ -529,10 +630,10 @@ final class Foo implements Struct {
529630
> [!CAUTION]
530631
>
531632
> Support for `AtomicInt` in FFI structs is meant to enable atomic access to
532-
> fields without requiring developers to go through `Pointer` based atomic APIs.
533-
> It is **not** meant as a way to interoperate with structs that contain
534-
> `std::atomic<int32_t>` (C++) or `_Atomic int32_t` (C11) because these types
535-
> don't have a defined ABI.
633+
> instance variables without requiring developers to go through `Pointer` based
634+
> atomic APIs. It is **not** meant as a way to interoperate with structs that
635+
> contain `std::atomic<int32_t>` (C++) or `_Atomic int32_t` (C11) because these
636+
> types don't have a defined ABI.
536637
537638
### Memory Model
538639

@@ -779,19 +880,19 @@ $$
779880
\forall i\leq j . \mathtt{Rel}(l, i) \leq_\mathtt{asw} \mathtt{Acq}(l, j)
780881
$$
781882

782-
##### Shared fields
883+
##### Shared instance variables
783884

784-
There can only be a single initializing store for any shared field. All other
785-
accesses are _not_ required to be atomic. However per definition of
885+
There can only be a single initializing store for any shared instance variable.
886+
All other accesses are _not_ required to be atomic. However per definition of
786887
$\leq_\mathtt{hb}$ relation all initializing stores happen-before other accesses
787888
to the overlapping locations. This means that if one thread creates an object
788-
and publishes it to another thread via a shared field - another thread can't
789-
observe object in partially initialized state. Implementations can choose to
790-
guarantee this property by inserting appropriate barriers when creating objects,
791-
however that would be a waste for objects that are mostly used in an
792-
isolate-local manner. Instead, given current restriction that only
793-
deeply immutable objects can be placed into shared-fields
794-
implementations can instead choose to implement shared fields using
889+
and publishes it to another thread via a shared instance variable - another
890+
thread can't observe object in partially initialized state. Implementations can
891+
choose to guarantee this property by inserting appropriate barriers when
892+
creating objects, however that would be a waste for objects that are mostly
893+
used in an isolate-local manner. Instead, given current restriction that only
894+
deeply immutable objects can be placed into shared instance variables
895+
implementations can instead choose to implement shared instance variables using
795896
_store-release_ and _load-acquire_ atomic operations. This would guarantee
796897
happens-before ordering for initializing stores. We however do not _require_
797898
such implementation and consequently developers can't rely on this in their

0 commit comments

Comments
 (0)