Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I am not an expert in Rust's mechanics and only looked at it superficially, but I got the impression that shared references can't be guaranteed (to not exist in mutable ways) all the way through an object graph. I am not surprised if I am wrong about that. It wasn't the main deciding factor for me...

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



In safe Rust, all mutation must be either:

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


What issues did you run into on the JVM with Java/Scala? I spent 5 years working up and down the entire akka stack so I'm curious to learn from your point of view. Also did you try akka typed or classic?


I'm not the parent poster but I'll have a go. Please feel free to correct me if I've got parts wrong, I'm hoping to learn something too.

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.


Also, serialization and object versioning challenges. I think I mentioned this in another comment but the lack of static typing in erlang/elixir tends to fence your composition model to where these sorts of problems are elided or explicitly handled in code.

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.


There are a couple of Rust actor frameworks, the most popular being actix [0] and bastion [1]

0: https://github.com/actix/actix 2: https://bastion.rs/


There are some escape hatches to get shared mutable reference. But in general those are impossible in rust.


Are they really "escape hatches"? Shared mutable state is as simple as `Rc::new(RefCell::new(x))`, or `Arc::new(Mutex::new(x))` for thread safety.


`Arc<Mutex<T>>` is not a shared mutable state. You cannot have two mutable reference at the same time. To mutate stuff inside the mutex, you got to hold the mutex, which guarantees that there are only one mutable reference at any given moment.

`RefCell` is one "escape hatches".


Worth noting that RefCell still enforces single-mutable-reference, it just does so at runtime. It lets you do things that could never be verified statically, but if you ever actually violate the condition, you'll get a panic (which is still better than memory unsafety).


Should be noted that this only works across threads.

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.


That would require getting into unsafe { } code and passing along a raw pointer, wouldn't it? Nothing regarding safety can be assumed about unsafe code at all


Nope, since when do you need to do that when using file IO or database connections?

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 { }.


What I'm not seeing is how the actual value contained inside a RefCell would get modified out from underneath your code without going through its checked methods, unless something has a raw pointer directly to its contents


Because you aren't looking at it from the context of data races anywhere on the application, and focusing on RefCell alone instead.

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.

https://doc.rust-lang.org/nomicon/races.html




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: