Shrug Having tried like every possible method of async I/O with Ruby to eek out moar good perfz back in the day... including various actor implementations (sup, JRuby and Akka)... nothing compares to just using BEAM if you want the actor paradigm. The cooperative scheduling problem is a really enormous pain in the ass most of the time.
Do you really want an actor system?
Actors are non-determinsitic, extremely prone to difficult-to-debug race conditions, and inherently stateful. They're a classic concurrency foot-gun from the dark old ways of doing things.
Don't we want to be moving away from these models that we know trip people up? Can't we do better than this for Ruby?
Sure you're not thinking of threads? Actor's have only really been done in Erlang, and more recenetly in Akka and Orlean's. AFAIK, there aren't any other paradigms other than CSP or threads for concurrency.
Actor's can have race conditions, same as any other concurrent system but I've personally never actually had any given the design patterns in OTP and Elixir. It's really helpful (to me) that each actor is always deterministic in itself and doesn't share data, only messages. It also maps nicely onto multicore.
No I'm thinking of actors - stateful, non-determinstic, racey actors. A minefield of classic concurrency bugs!
> Actor's can have race conditions, same as any other concurrent system
Deterministic systems like fork-join don't ever have race-conditions.
> is always deterministic in itself and doesn't share data
But the global state of all the actors is shared. If an actor you send a message to responds differently due to its state then let's be honest you're implicitly sharing that state.
> It also maps nicely onto multicore.
It maps directly to multicore, right. Don't we want something higher level and safer than directly mapping to our hardware?
https://hexdocs.pm/elixir/Task.html#module-async-and-await
[0] determinism can go out the window once there's an error. Can you safely say you know what happens in ruby if you are running async and one of your concurrently running async functions throws?
The nice thing in elixir there is zero ambiguity as to what happens if you have (N) async functions running and one of them crashes -- it brings down all of the other threads and triggers their respective resource cleanups in exactly the way that they do as actors, since Task is a fairly trivial wrapping of erlang's actors. (Or you have choices!!, you can async_nolink if you want your async function to plow forward even if its parent dies due to the crash of one of its siblings)
I think there is still some confusion since you suggest fork/join but that's only for threads and won't work over a network.
Map-reduce is fork-join and runs just fine over a network.
Well yes systems of actors contain race conditions. However, so does Rust's or Go's concurrency primitives. The only way to completely avoid race conditions I know of would be formal methods via TLA+ or similar. From my brief looks into TLA+ it'd appear to match actor implementation models pretty well (messages to actors yield new states).
> But the global state of all the actors is shared. If an actor you send a message to responds differently due to its state then let's be honest you're implicitly sharing that state.
It's not really whether a program has state, but how that state is accessed and modified. In the actor model, only an actor itself can modify it's own data which makes it easier to reason about many classes of concurrency problems. Especially actions within an actor itself are deterministic (generally speaking). This makes a lot of problems become very straightforward to model without needing locks everywhere. The concurrency problem is shifted to modeling the behavior of a group of actors, which helps enforce separation of concurrency concerns from the lower level implementation details (with a good framework at least).
Since I was asking and thinking about Ruby getting (preemptive) actors initially, this frame of thought seems odd to me. The entire foundation of Ruby is built on objects that contain implicit state and on those objects behaving differently based on their state even to the point of checking for unhandled "messages" via `#method_missing`.
Th fork/join model can be emulated rather well as @dnautics described in a sibling comment. It's close to the model used in Elixir's Phoenix web server where each request just forks an Erlang process to handle it.
At the end of the day, issues with races in the actor paradigm are little different than communicating with remote programs as either fork'ed processes or network programs. Just fork'ing a program may avoid some concurrency issues by not doing any useful work by itself. If you do use fork/join to service http requests via sockets you're now relying on implicit state from the OS to do the heavy lifting.
If anything Linux's new `io_uring` works by treating a process like an actor where the process & kernel pass messages via a "messsage queue" which means your forks will just be acting as pseudo-actors anyway. Side note, on Linux doing this saves a lot of context switches by not preempting the OS process' call stack via a direct syscall every time it has something to communicate to the kernel. That gives a big speed bump to IO heavy programs. There can be significant performance benefits to making programming models work more like the hardware. I expect that actor models will grow to be a better performance match as core counts continue rising.
> Don't we want something higher level and safer than directly mapping to our hardware?
Certainly! ... if it makes acceptable tradeoff's in other areas. Though Actor's are really more of a base system to build higher level constructs that are easier/safer to use. Most programming shouldn't be done using your own concurrency primitives, but building on well thought out and tested libraries.
Mainly though, the idea of a Ruby with preemptive actors seems fun to me. Perhaps something you could map to lego robot's for kids.
The fork-join model does allow you to completely avoid race conditions, because tasks are always created in a deterministic order, each can only work with the immutable input they get, and tasks join also in a deterministic order.
(I think when I say fork-join you're thinking processes fork and join - I mean the abstract concept of fork-join. Map-reduce is an example of a fork-join model and is deterministic and race-free by construction with no proof system needed.)
So in practice we glady throw more memory and processes at the problem.
I have used ruby since 2013 and started on rails but quit using rails in 2015. I still use ruby all the time though, mostly for scripts/automation that violate my rule on bash v. other lang for scripts (basically do I need arrays, maps, or to parse json beyond simple extractions that jq is great at). I do have a couple sinatra services now though that I maintain. Sinatra is wonderful with simple needs like mine.
I don't know how much of an anomaly I am though, interested in hearing from others as well.
Edit: not looking to debate bash vs other langs here. Of course you're free to do such below, but I won't be engaging (got to focus on work and have had the debate many times and I don't think any of us will ever convince the other. It's become religion at this point).
Also I use Elixir/Phoenix these days for use cases that used to be rails
What held me back so far is that a lot of simple B2B, low volume stuff doesn't seem to benefit from the high parallelism that BEAM brings.
But I'm wondering if it brings enough other advantages that I shouldn't view it that simplistically.
So to start with, Phoenix LiveView really is a game changer, you should have a look at Elixir just for that. This is the talk which really bluffed me when I first saw it: https://www.youtube.com/watch?v=MZvmYaFkNJI.
For the other upsides, I like a little bit better the way everything is architectured in Phoenix, there's much less magic and it's easier to follow the data flow and what is going on.
I would recommend trying it and seeing how you like it. I've basically dropped Python which was my daily language in favor of Elixir. I find I get a higher top bound to what I can do, high-level expressive but less magical code and a bunch of capabilities that aren't typically feasible with other runtimes (state handling, resiliency and stuff). The parallelism I get for free with Phoenix whether I try or not.
I'd watch Sasa Jurics talk on the heart of elixir and erlang to get more of the technical advantages laid out quite well.
I've run both Rubinius and jruby (with rails), both gave me significant performance gains.