dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.1k stars 1.56k forks source link

Dart lacks Actor model implementation in core libraries #50063

Open gaaclarke opened 1 year ago

gaaclarke commented 1 year ago

This tracker is for issues related to:

Description

After a few years of writing Dart code I find myself writing the same code over and over again to implement the Actor model. We should have a core library implementation for it. There is too much boilerplate that is easy to get wrong and the bar is too high for offloading work from the root isolate.

Here are the required steps to implement the Actor model with the core libraries:

I inevitably end up writing something like this each time:

enum _Codes {
  init,
  add,
  ack,
}

class _Command {
  final _Codes code;
  final Object? arg0;
  _Command(this.code, {this.arg0});
}

void _isolateMain(SendPort sendPort) {
  ReceivePort receivePort = ReceivePort();
  sendPort.send(_Command(_Codes.init, arg0: receivePort.sendPort));
  receivePort.listen((dynamic message) async {
    final _Command command = message as _Command;
    // handle command
  });
}

void main() async {
  ReceivePort receivePort;
  Isolate isolate = await Isolate.spawn(_isolateMain, receivePort.sendPort);
  Queue<Completer<void>> completers = Queue<Completer<void>>();
  Completer<SendPort> sendPortCompleter = Completer<SendPort>();
  receivePort.send((value) {
    _Command command = value;
    if (command.code == _Codes.init) {
      sendPortCompleter.complete(command.arg0);
    } else if (command.code == _Codes.ack) {
      completers.popBack().complete();
    }
    // etc, etc
  });
  SendPort sendPort = await sendPortCompleter.future;
  Completer<void> ack = Completer<void>();
  completers.pushFront(ack);
  sendPort.send(_Commend(_Codes.add, arg0:1));
  await ack;
}

Agents

As an example of implementing the Actor model in Dart I created the package isolate_agents.

Here is what that looks like:

import 'package:isolate_agents/isolate_agents.dart';

void main() async {
  Agent<int> agent = await Agent.create(1);
  agent.send((int x) => x + 1);
  assert(2 == await agent.exit());
}

There is no need to do any of that boilerplate to get communication happening with an Isolate, thus being a full implementation of the Actor model.

We don't need to adopt the API I created in isolate_agents, but we should have some solution for users in the core libraries.

lrhn commented 1 year ago

So you have state that you store in an isolate, then you repeatedly run functions against this state in the isolate, and wait for the result.

I don't think we have anything precisely matching that in dart:isolate or package:isolate.

I'd probably implement something on top of IsolateRunner, but it would still take some boilerplate. It's something I wouldn't mind adding to package:isolate (if it wasn't discontinued). I don't think it belongs in the platform libraries, it's just a little too specialized.

Maybe what we need would be an API like

class Isolate {
   // ...
   static Future<void> spawnChannel(void Function(RawReceivePort input, SendPort output, Future<void> remoteClosed) local, 
       void Funtion(RawReceivePort input, SendPort output, Future<void> remoteClosed) remote ) { ... }

which sets up communication channels that you can use to send command back and forth, and you can close the receive port to tell the remote that you stopped listening.

gaaclarke commented 1 year ago

So you have state that you store in an isolate, then you repeatedly run functions against this state in the isolate, and wait for the result.

Yep, in the actor model there is a entity that represents a thread of execution, state that lives on that thread, a messaging system to communicate with that state asynchronously and serially.

I don't think we have anything precisely matching that in dart:isolate or package:isolate.

I'd probably implement something on top of IsolateRunner, but it would still take some boilerplate. It's something I wouldn't mind adding to package:isolate (if it wasn't discontinued). I don't think it belongs in the platform libraries, it's just a little too specialized.

I think we should go look at usages of Isolate in github because I think you are underestimating how often this pattern is repeated. However I wouldn't be surprised if the high bar for implementing is artificially guiding users to use the API sub-optimally (ie spawning many isolates instead of having one long lived isolate that you can communicate with).

When using isolates there are 3 cases: 1) No sendports are used: The isolate is fired and it never communicates with it's spawner. There is just one way communication from spawnIsolate's argument. This should represent a very low percentage of the usages since this is error prone since there is no synchronization about when the operation is done. 1) One sendport is used: An isolate is spawned, does some calculation on the argument from spawnIsolate and returns 1 or more results. The boilerplate is not bad in this case, but even this usage would benefit from an Actor. This usage is probably ~50% of the usage, but in a world where Actors are available it would be less. 1) Two sendports are used: An isolate is spawned and communicated with, performing many operations and returning many results. This is the case where there is the most benefit of having an Actor and I suspect this represents at least 50% of the desired usage.

Here are the languages on the TIOBE top 50 that have actors in their core library: 1) Scala 1) Erlang 1) Clojure

Although I admit that isn't that impressive and doesn't bolster my case.

Maybe what we need would be an API like

class Isolate {
   // ...
   static Future<void> spawnChannel(void Function(RawReceivePort input, SendPort output, Future<void> remoteClosed) local, 
       void Funtion(RawReceivePort input, SendPort output, Future<void> remoteClosed) remote ) { ... }

which sets up communication channels that you can use to send command back and forth, and you can close the receive port to tell the remote that you stopped listening.

Yea, I think some function that does the SendPort handshake would be an improvement. I'm not following exactly how this one would work. I think we could come up with something. Maybe an object Channel that would comprise a 2 way communication a SendPort and ReceivePort. I'll think about it and add it later.

gaaclarke commented 1 year ago

One other thing that occurred to me, while not many of the most popular languages have the Actor model, they do have shared memory between threads. Dart, Clojure and Erlang do not have shared memory. That's why it is a good fit for Dart and why I think there is a need for it. We built this wall between threads but haven't given users the tools to work through them efficiently.