Skip to content
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

Merged
merged 9 commits into from
Apr 9, 2024
Merged

Shared Memory Multithreading #3531

merged 9 commits into from
Apr 9, 2024

Conversation

mraleph
Copy link
Member

@mraleph mraleph commented Dec 21, 2023

Initial proposal attempting to introduce shared memory multithreading.

/cc @leafpetersen @lrhn @eernstg @munificent
/cc @a-siva @alexmarkov @mkustermann @dcharkes @liamappelbe @chinmaygarde
/fyi @syg @kevmoo

@mraleph mraleph changed the title Share Memory Multithreading Shared Memory Multithreading Dec 21, 2023
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
@Piero512
Copy link

Piero512 commented Jan 3, 2024

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.

@mraleph
Copy link
Member Author

mraleph commented Jan 25, 2024

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
@lrhn @yjbanov

@Piero512
Copy link

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.
On the callback of the sendport, from the shared isolate, which must run in an executor, I atomically fetch the stored integers (because I'm assuming that the executor and the callback provided to miniaudio run in different threads), and report back through the sendport of the main isolate after receiving the report message from the main isolate.

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?

working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
/// [Shareable] instances can be shared between isolates in
/// a group and mutated concurrently.
///
/// A class implementing [Shareable] can only declare fields
Copy link
Member

@lrhn lrhn Jan 25, 2024

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).

working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
working/333 - shared memory multithreading/proposal.md Outdated Show resolved Hide resolved
@gmpassos
Copy link

gmpassos commented Feb 1, 2024

Thank you for this proposal; it will be crucial for achieving performant parallelism in Dart.

I would like to suggest an additional level of Shareable objects. Since a Shareable already represents the scope of memory/data that can be shared between isolates/threads, it would be beneficial to also facilitate a synchronization model for manipulating that shared data:

/// 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
  
}

@gmpassos
Copy link

Please, don't ignore the ShareableSynced idea.

Best regards.

@mraleph
Copy link
Member Author

mraleph commented Apr 9, 2024

I have some small tweaks here and there and just going to land this.

@mraleph mraleph merged commit 060b26d into dart-lang:main Apr 9, 2024
3 checks passed
@mraleph mraleph deleted the shared-memory branch April 9, 2024 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.