-
Notifications
You must be signed in to change notification settings - Fork 226
Add shared native memory proposal #4515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
working/333 - shared memory multithreading/shared_native_memory.md
Outdated
Show resolved
Hide resolved
working/333 - shared memory multithreading/shared_native_memory.md
Outdated
Show resolved
Hide resolved
```dart | ||
@pragma('vm:deeply-immutable') | ||
final class ScopedThreadLocal<T> { | ||
/// Creates scoped thread local value with the given [initializer] function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, but outside of isolate-group-bound callbacks, in the context of standard isolates, does ScopedThreadLocal
instance behave like a static
field? Isolates in the end can migrate from one thread to another.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that isolates can't move between threads in synchronous calls and this API is fully synchronous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will there be something that would prevent this infrastructure from being used in asynchronous context?
static Future<String> iterableToShortString(...) {
return toStringVisiting.use((toStringVisitingValue) async {
await Future.delayed(Duration(seconds: 10));
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use it. But it is not going to do anything useful because the value of the scoped thread local will reset once the synchronous execution completes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So toStringVisiting
can become unbound after await Future.delayed(Duration(seconds: 10))
(in unlikely event) of isolate switching to a different thread? Realistically we are keeping isolate/(vm)thread affinity, so switching should not happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not related to moving between threads at all. ScopedThreadLocal
are valid within the scope of synchronous execution (hence the name). So if you do stl.use(f)
or stl.with(value, f)
once synchronous execution of this expression completes stl
(e.g. use
or with
completes) is unbound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was curious about the case when f
is asynchronous. Would that result in ScopedThreadLocal
values bound only for initial synchronous part of f
execution? In the example above - before await Future.delayed(Duration(seconds: 10));
?
Document constructors memory model
cc @alexmarkov for memory model feedback |
``` | ||
|
||
Note that `runSync` can only enter an `Isolate` when it is not used by | ||
another thread. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How will the caller be able to know that?
The other isolate must be back at the event loop, and probably not just the microtask loop.
I guess the isolate can set a flag when its event loop is empty and it's waiting for new events.
You could potentially have a way to check that flag, but that won't guarantee that the isolate didn't get and started processing an event immediately after you checked.
you invoke this function `f` will be executed in the context of the given | ||
isolate and then if it produced a `Future` we exit the isolate and block | ||
the current thread, while allowing the event loop for that isolate to run | ||
normally until returned future produces result. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That can be implemented using a synchronous/blocking receive port.
It's possible that there is a use for this, but it's rarely a good idea to block your own isolate.
external void withInitialized<R>(R Function(T) f); | ||
|
||
/// Returns the value specified by the closest enclosing invocation of [with] or | ||
/// throws [NotBoundError] if this [ScopedThreadLocal] is not bound to a value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can just use StateError
.
/// Throws [NotBoundError] if this [ScopedThreadLocal] does not have an initializer. | ||
external void withInitialized<R>(R Function(T) f); | ||
|
||
/// Returns the value specified by the closest enclosing invocation of [with] or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect I'll use the pattern:
String toString() {
if (!_toStringVisiting.isBound) {
return _toStringVisiting.runWith([], (_) => toString());
}
var visiting = _toStringVisiting.value;
//...
}
in every function that depends on the value.
It can't all be abstracted into a helper method like it's it's today, the initialization must be on the main trunk of the execution tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you use withInitialized
instead?
Given
@pragma('vm:shared')
final toStringVisiting = ScopedThreadLocal<List<Object>>(() => <Object>[]);
you can write
String toString() {
return toStringVisiting.withInitialized((toStringVisitingValue) {...})
}
which will only invoke initializer if (isolate-group) toStringVisiting
instance of ScopedThreadLocal
is not bound on this thread yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to double the number of stack frames and generate extra closures if I can avoid it.
I don't expect the call to be a tail-call.
|
||
/// Execute [f] initializing this [ScopedThreadLocal] using default initializer if needed. | ||
/// Throws [NotBoundError] if this [ScopedThreadLocal] does not have an initializer. | ||
external void withInitialized<R>(R Function(T) f); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with
may be a reserved word. I think runWith
would be the traditional name.
} | ||
``` | ||
|
||
### Synchronization Primitives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are use-cases?
Communicating between isolates?
SameLoc_{ww}(w, w') &\triangleq Loc_w(w) = Loc_w(w') \\ | ||
Write(e) & \triangleq Loc_w(e) \neq \emptyset \\ | ||
Unordered(e) & \triangleq \neg(\mathtt{Atomic(e)} \vee \mathtt{Init}(e)) | ||
\end{align*} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't review this, because I can't read how it looks. 😔
OK, I copied it into a markdown renderer.
Now I don't know what "≜" means. Is this defining functions or relations?
Says "functions and predicates", so probably functions (some to Bool).
The point of markdown is that the source is readable. LaTeX math isn't, so it feels like it's missing the point.
I was going to suggest using Unicode instead, but ∨
isn't much better then \vee
.
Consider just using ASCII and words:
- LocR(r) := [Addr(r), Addr(r), + |DataR(r)|]
- LocW(r) := [Addr(r), Addr(r), + |DataW(r)|]
- Loc(e) := Union(LocR(e), LocW(e))
- Overlap(e) := Intersection(LocR(e), LocW(e))
- SameLocWR(w, r) := LocW(w) = LocR(r)
- SameLocWW(w, w’) := LocW(w) = LocW(w’)
- Write(e) := LocW(e) is not empty
- Unordered(e) := not Atomic(e) and not Init(e)
When object is constructed all field initializers (i.e. those provided in the | ||
body of the class or in the initializer list) result in $\mathtt{Init}$ | ||
write events. The same applies to initialization of individual elements of | ||
`TypedData` objects with `0`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And platform lists? (Probably want all object creation to act like initialization, also for collection literals.)
|
||
#### Isolates | ||
|
||
Every `SendPort.send` generates a numbered event $\mathtt{Send}(p, i)$. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the number arbitrary or monotonically increasing?
(The "numbered" makes it sound like the numbers are 1, 2, ..., but it doesn't actually say that.)
|
||
$$ | ||
\forall p\,i . \mathtt{Send}(p, i) \leq_\mathtt{asw}\mathtt{Recv}(p, i) | ||
$$ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we also want:
∀ p, i, j, Send(p, i) ≤asw Send(p, j) ↠ Recv(p, i) ≤asw Recv(p, j)
? That is, sends on the same port are not reordered, but that's proably only true if the sends are in the same isolate.
This extracts relevant parts of the bigger proposal into a separate proposal.
The following things have changed:
AtomicInt
class - which can be constructed as a view onPointer
s orTypedData
objects. This is a departure from the previous "zoo of extension methods" design. We should discuss if it is actually better.ScopedThreadLocal
class which should address some known problematic usages of static state in core libraries.@lrhn @mkustermann @aam can you take a look?
@aam could you give me the current state of core-library APIs (which already work from IG bound code, which you are fixing and which we will not support). I want to align on what we think is MVP here and include this into the proposal.