-
Notifications
You must be signed in to change notification settings - Fork 206
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
Shared Memory Multithreading #3531
Conversation
Hello! I was reading the proposal and kinda resonated with me the issue of not being able to do parallel operations that are thread safe, because they're not really operating on the same data in the same class, because the class that someone created is not shareable, so I believe that a better way would be to have concepts in the language that would be similar to Rust's Send and Sync. I am aware that currently the Dart language does not statically track the usage of objects to be able to have the compiler implement Send automatically, but it would make the easiest transition as it would enable a lot of usecases with no changes to library code. About sync, well, that's a little bit harder. I just wanted to point out that I am not a heavy user of Rust, so if anyone disagrees with it, please enlighten me on why. |
Thanks to everyone for the initial feedback. I have done some major rewrite removing things which complicated the proposal (e.g. treatment of generics and functions) from the proposal. I have added a section which tries to better explain the motivation for suggesting shared memory multithreading as well as a section which covers various popular programming languages to illustrate how they approach the same problem. Please take another look. I think this is getting to the point where I will take another round of comments to clean the structure and writing and then I probably want to land this PR and take discussion to the issues. cc @aam @brianquinlan @dcharkes @kevmoo @liamappelbe @loic-sharma |
This is nice. I like the comcept of the executor. Have you considered how it would be intended to be used? I was thinking something in terms of say, async communication from regular isolate, and shared isolate state. To use your miniaudio example, say I'm running a resampling operation from a microphone,so I hook up dart:ffi to generate my function pointer to implement my data callback, where I read from mic, do some resampling (in Dart) and return, and use some integers to keep a count of how many frames I have converted (for reasons). I imagine keeping track of this operation and dynamically modifying it from regular isolated world, would be easiest if I have a two way communication with that code, so say I build it using dual sendports. For that example, the executor would need to be given the shared isolate (or isolates) to run in the same context as the ffi callback. Of course an easier design would be to just have the ffi callback send the updated count to the sendport, but this is just for the sake of argument. What do you think of this example? Do you see it working that way? |
/// [Shareable] instances can be shared between isolates in | ||
/// a group and mutated concurrently. | ||
/// | ||
/// A class implementing [Shareable] can only declare fields |
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.
While definitely possible, this does break getter/field abstraction to some extent.
It means that whether an otherwise extensible class (not, e.g., final
and has a public generative constructor) can be subclassed by a class which implements Sendable
, depends on whether the superclass has an instance variable declared with a non-sendable type.
That's not currently important, or visible at the class boundary. Only the interface matters, and even only the public interface when extended from a different package. And the interface only had a getter, whether the class has a field or a getter decoration.
That makes changing a getter to a field, or just adding a cache for a getter, a potentially breaking change.
I see no good way to address that, other than the author of the superclass somehow promising, in the class declaration, that they won't be adding any non-sendable fields.
Say by having a modifier, strawman: sendable_compatible
, which is not being Sendable
(that would force all subtypes to be sendable, which is not what we want).
Then it's a compile time error for a Sendable
class, or a sendable_compatible
class, to extend a class, or mix in a mixin, which is.not Sendable
or declared as sendable_compatible
. Which Object
is, so we can start there.
It's an error to add a non-sendable fields to a sendable_compatible
declaration, and it's considered a breaking change to remove the modifier from a class, like it is to make any extensible class Sendable
.
That gives us a class hierarchy where the top classes are sendable-compatible, until a subclass chooses to either be sendable, which all its subclasses will have to be too, it's an interface that's inherited, or to not be sendable-compatible, by not having the modifier (whether it adds an unsendable field or not, it has now claimed the ability), and then it's extending subclasses must be non-sendable(-and-compatible).
Thank you for this proposal; it will be crucial for achieving performant parallelism in Dart. I would like to suggest an additional level of /// A [Shareable] object with synchronized manipulation of internal data.
/// Any call to a method/getter/setter of a [ShareableSynced] object will
/// occur in a synchronized way, locking the object, executing the
/// method/getter/setter, and then unlocking the object. This ensures
/// that the internal data of the [ShareableSynced] is manipulated in a
/// thread-safe manner.
///
/// All internal fields of a [ShareableSynced] object need to be private,
/// isolating the internal data from unsafe manipulation.
///
/// Methods/getters can only return primitive types, immutable types or
/// [Shareable] objects, avoiding any leakage of internal objects.
///
/// Methods, getters, and setters will always perform a non-reentrant
/// synchronization over the object.
///
/// Sub [ShareableSynced] objects will also trigger their own synchronization.
///
abstract interface class ShareableSynced extends Shareable {
}
class Counter implements ShareableSynced {
int _n = 0;
int get() => _n;
int increment() => ++_n;
}
void main() async {
final counter = Counter();
print(counter.get()); // 0
var isolateRun1 = Isolate.run(() {
print(counter.increment()); // 1 or 2
});
var isolateRun2 = Isolate.run(() {
print(counter.increment()); // 1 or 2
});
await Future.wait([isolateRun1,isolateRun2]);
print(counter.get()); // always 2
} |
Please, don't ignore the Best regards. |
I have some small tweaks here and there and just going to land this. |
Initial proposal attempting to introduce shared memory multithreading.
/cc @leafpetersen @lrhn @eernstg @munificent
/cc @a-siva @alexmarkov @mkustermann @dcharkes @liamappelbe @chinmaygarde
/fyi @syg @kevmoo