Having done concurrency in Java and Rust, my experience is that Rust's concurrency primitives are an order of magnitude better than Java's. I haven't tested C#'s.
C# doesn't have Send and Sync that is true. It frequently does not need either because it uses GC instead of affine types for automatic memory management. Synchronization is indeed "just don't write bugs", where Rust offers a massive upgrade, but .NET CoreCLR's memory model is more strict than C one, like object reference assignment having release semantics, so quite a few footguns are luckily avoided: https://github.com/dotnet/runtime/blob/main/docs/design/spec...
'&' and '&mut', however, are your 'ref readonly' and 'ref' respectively.
Is there anything in C# which ensures what Rust does statically, that you must acquire a lock to access data protected by a mutex? Rust also has MutexGuard not be sendable i.e the mutex won't be released on a different thread from which it is acquired.
With traditional mutex APIs it's just far too easy to get it wrong. I think you just have to structure your thread-related APIs to be misuse resistant. As humans we're just not good enough at not making mistakes.
> I think you just have to structure your thread-related APIs to be misuse resistant
The premise of this stays. C# approaches this in a more traditional way, with exposing the set of synchronization primitives. It's a step above C and, usually, C++ still because you don't need to e.g. have an atomic reference counting for objects shared by multiple threads.
Concurrent access itself can be protected as easily as doing
lock (obj) {
// critical section
}
This, together with thread-safe containers provided by standard library (ConcurrentDictionary, ConcurrentStack, etc.) is usually more than enough.
What Rust offers in comparison is strong guarantee for complex scenarios, where you usually have to be much more hands-on. In C#, you can author types which e.g. provide "access lease", that look like 'using var scope = service.EnterScope(); ...`, where using turns into a try-finally block, where finally that calls .Dispose() on the scope is guaranteed to be executed.
It's a big topic, so if you have a specific scenario in mind - let me know.
Thanks! To be fair, there are certain advanced scenarios which Rust's mutex model can't handle either -- sometimes you want to protect writes via a mutex, but allow reads without them (maybe you're okay with torn state on reads). This is a rare, expert use case with architecture-specific considerations that must be handled with care.
I do think Rust's mutexes handle almost every use case that can be thrown at them, though, and in a way where it's next to impossible to get it wrong. I think if you're writing a browser engine in the 21st century you should bake in parallelism and concurrency from the start, and Rust is the most suitable language to do that in.