My primary factor was "Actor Model" and I was struggling with Erlang's lack of static types, so that put Pony in the focus. An Actor Model framework on top (I recalled there was one for Rust) had given me "questionable" results with Akka (due to compromises/constraints in the JVM and Java/Scala).
HTH
- Statically verified to be unique (&mut)
- Dynamically verified to be unique (RefCell)
- Behind a lock (Mutex, RwLock)
- Explicitly opt into weaker (but still consistent) guarantees (atomics)
- Some other abstraction built on those primitives (like channels)
Unsafe does exist, but it's something that the code must explicitly opt into, and they rules that must be followed are generally "the same" across the whole Rust ecosystem (rather than just accidentally stumbling into something that is OK in "normal" Scala but breaks some weird Akka rule).
In any case, the language enforces that you are self-consistent: if you use locks as your escape hatch for something then the code won't compile if you try to access it without an active lock.
In other languages, lets say Erlang as an example, actors themselves do not have concurrency concerns. The receive loop/handler just executes everything as though it's single threaded. The code is very easy to reason about. It also applies backpressure in that if your synchronous loop is blocked, your mailbox will fill up.
If you need concurrency, it's done by talking to other actors, which can be processing on another thread under the hood. But the thread part of it is managed and you are not dealing with threads per se, you just know that if you have multiple cores and you send a message to another actor, it can be scheduled to run on another core safely.
If you want to run a bunch of tasks in parallel, you could use a pool of actors up to around the number of cores you have, and the parallelism is at maximum the number of actors in the pool.
Sorry if i'm over explaining, I just wanted to set the stage.
One of the benefits of this arrangement is if something is going slowly you can order the list of actors by biggest mailbox and you can see where your bottleneck is. And if you are using a lot of memory you can just order the actors by the memory usage and you can see where the big state lives.
With akka actors, instead of just dealing with the actors and actor pools, they suggest you make actors non-blocking. The way you do this is with Futures. Suddenly all the simplification of the actor model goes out the window. It mixes an async programming style with an actor model that doesn't need to be async! So you have the negatives of asynchronous programming and very few benefits of the actor model. I realise
How do you identify the bottlenecks of the system? Maybe your execution context is full - actually I'd love to know how people debug their execution contexts in general.
Last I checked, execution contexts would spawn new threads as well, so not only are they heavyweight (compared to erlang processes) but you have the operating system schedule them instead of the thing that knows how to best schedule them which is your language runtime.
But AFA Async, I guess I have thoughts.
> With akka actors, instead of just dealing with the actors and actor pools, they suggest you make actors non-blocking. The way you do this is with Futures.
I don't know how things work in JVM/Akka specifically but on the .NET side the use of async is 'Tolerable'. We have a ReceiveActor that lets you wire up your message type to an async method and it handles all of the stashing/etc internally until the future completes. You have to type a couple extra words but they're the same words you have to type everywhere else you use async.
With the other sugar .NET gives you it's really not too bad. In the system our team built, the main piece of 'blocking' code we have is a call to a C library that does not have any real 'async' bindings. The rest were things like loggers, although typically if 'logging' is blocking either you're logging too much or the rest of the system is probably not in a good state anyway. (edit: FWIW, the 'block' is 80-100ms and constant, we can live with it for our case)
> How do you identify the bottlenecks of the system? Maybe your execution context is full - actually I'd love to know how people debug their execution contexts in general.
Interestingly, Akka.NET doesn't quite have this sort of problem.The default Dispatcher (At least that's what we call the base type) runs on the .NET Thread pool which has a good degree of 'self tuning', and you can peek at the number of threads vs what the pool maxes out at with 4 lines of code or so. However in .NET we have 'SynchronizationContexts' which result in the need for special dispatchers for things like a UI update (as most UI frameworks have their own context for handling UI).
> One of the benefits of this arrangement is if something is going slowly you can order the list of actors by biggest mailbox and you can see where your bottleneck is.
You could probably hack together something off of akka visualmailbox [0] as it shows how to grab metrics from 'inside' a mailbox. I did a toy port to .NET and while on that side I had to do a lot of 'configuration' and still need to create metrics collecting mailboxes for all the types (we don't have traits...) but it seemed to actually work not-bad.
`RefCell` is one "escape hatches".
Nothing can be assumed regarding data races if it happens to be a shared value of some sort modified via IPC or other kind of APIs across multiple processes accessing the same resource.
The library code down at the bottom layer might do that, depending on how the bindings to the APIs are implemented, but it won't be necessarily exposed to the code you are writing yourself.
For example, doing SQL statements isn't unsafe { }.
Yes, Rust prevents data races when several threads try to modify a memory location inside the same process.
This is just a special case of data races, which may take several forms.
If several processes, or even threads are accessing an external resource, like a database, each of them can issue UPDATE statements on the same record, and it is impossible to validate which value you will get back, unless it is done inside a proper transaction block.
Ensuring that a SQL data race doesn't happen, might be critical, e.g. several people to the same plane seat, yet there is nothing on the RefCell or not using unsafe {} that can enforce it.
I would advise to read the "Data Races and Race Conditions" chapter of Rustonomicon regarding what guarantees Rust actually provides, anything else is up to the programmer to take care they don't happen.
Pony is a completely new language based on the actor model where "threading" is a first-class operation and the type system is formally designed to make guarantees that make threads safer.
Pony really wants you to think differently about your problem and solution.
Much of Pony can be done with Arc/Mutex/&/move in Rust, but with special syntax, and it's that special syntax that kills the language frankly.
You can implement actors in Rust pretty easily. I've done it multiple times, including in a way that provides a pony-like nominal interface. It isn't as powerful, and it isn't as efficient, because frankly I'm not very good at this sort of thing and don't have time to work on it, but it's possible.
What pony gives is some syntactic sugar in some important places (the actor keyword, the be keyword) and a very strong runtime.
And saner defaults for type safety.
As much as I enjoy C++, the safety defaults due to backwards compatibility are mostly wrong.
EDIT: s/data races/race conditions
The borrow checker (in safe rust) always[0] prevents data races. It can't, however, prevent race conditions (but neither can pony do[1])
I typically think of a race condition an issue with concurrency where data might be modified by another thread which causes the program to return incorrect results.
Data race vs race condition:
A race condition happens when data/state is mutated because of the order in which concurrent processes occur. This could happen with threads, message passing, or many other ways.
I think this is distinct from deadlock which occurs when there is at least one circular dependency in the order when different "processes" must perform their operations.
As long as those data races originate from threads, it cannot prevent data races across processes, for example:
- Modifying the same file location
- Modifying the same table cell without transactions
- Talking to the same IO port
- Data sharing between CPU and GPU