A better concurrency story is one of the main goals of the Rust project, so improvements should be expected, provided we trust the project to achieve its goals. Full disclaimer: I have a high opinion of Rust and am invested in it. As requested, I'll try to avoid value judgements and describe differences rather than (IMHO) improvements.
Safe and unsafe Rust
"Rust" is composed of two languages: One that tries very hard to isolate you from the dangers of systems programming, and a more powerful one without any such aspirations.
Unsafe Rust is a nasty, brutish language that feels a lot like C++. It allows you to do arbitrarily dangerous things, talk to the hardware, (mis-)manage memory manually, shoot yourself in the foot, etc. It is very much like C and C++ in that the correctness of the program is ultimately in your hands and the hands of all other programmers involved in it.
You opt into this language with the keyword unsafe
, and as in C and C++, a single mistake in a single location can bring the whole project crashing down.
Safe Rust is the "default", the vast vast majority of Rust code is safe, and if you never mention the keyword unsafe
in your code, you never leave the safe language. The rest of the post will mostly concern itself with that language, because unsafe
code can break any and all of the guarantees that safe Rust works so hard to give you. On the flip side, unsafe
code is not evil and not treated as such by the community (it is, however, strongly discouraged when not necessary).
It's dangerous, yes, but also important, because it allows building the abstractions that safe code uses. Good unsafe code uses the type system to prevent others from misusing it, and therefore the presence of unsafe code in a Rust program need not disturb the safe code. All the following differences exist because Rust's type systems has tools that C++'s doesn't have, and because the unsafe code that implements the concurrency abstractions uses these tools effectively.
Non-difference: Shared/mutable memory
Although Rust places more emphasis on message passing and very strictly controls shared memory, it does not rule out shared memory concurrency and explicitly supports the common abstractions (locks, atomic operations, condition variables, concurrent collections).
Moreover, like C++ and unlike functional languages, Rust really likes traditional imperative data structures. There's no persistent/immutable linked list in the standard library. There's std::collections::LinkedList
but it's like std::list
in C++ and discouraged for the same reasons as std::list
(bad use of cache).
However, with reference to the title of this section ("shared/mutable memory"), Rust has one difference to C++:
It strongly encourages that memory be "shared XOR mutable", i.e., that memory is never shared and mutable at the same time.
Mutate memory as you like "in the privacy of your own thread", so to speak.
Contrast this with C++ where shared mutable memory is the default option and widely used.
While the shared-xor-mutable paradigm is very important to the below differences, it is also a quite different programming paradigm that takes a while to get used to, and that places significant restrictions.
Occasionally one has to opt out of this paradigm, e.g., with atomic types (AtomicUsize
is the essence of shared mutable memory).
Note that locks also obey the shared-xor-mutable rule, because it rules out concurrent reads and writes (while one thread writes, no other threads can read or write).
Non-difference: Data races are undefined behavior (UB)
If you trigger a data race in Rust code, it's game over, just as in C++. All bets are off and the compiler can do whatever it pleases.
However, it is a hard guarantee that safe Rust code does not have data races (or any UB for that matter).
This extends both to the core language and to the standard library.
If you can write a Rust program that doesn't use unsafe
(including in third party libraries but excluding the standard library) which triggers UB, then that is considered a bug and will be fixed (this has already happened several times). This if of course in stark contrast to C++, where it's trivial to write programs with UB.
Difference: Strict locking discipline
Unlike C++, a lock in Rust (std::sync::Mutex
, std::sync::RwLock
, etc.) owns the data it's protecting. Instead of taking a lock and then manipulating some shared memory that is associated to the lock only in the documentation, the shared data is inaccessible while you don't hold the lock. A RAII guard keeps the lock and simultaneously gives access to the locked data (this much could be implemented by C++, but isn't by the std::
locks). The lifetime system ensures that you can't keep accessing the data after you release the lock (drop the RAII guard).
You can of course have a lock that contains no useful data (Mutex<()>
), and just share some memory without explicitly associating it with that lock. However, having potentially unsynchronized shared memory requires unsafe
.
Difference: Prevention of accidental sharing
Although you can have shared memory, you only share when you explicitly ask for it.
For example, when you use message passing (e.g. the channels from std::sync
), the lifetime system ensures that you don't keep any references to the data after you sent it to another thread. To share data behind a lock, you explicitly construct the lock and give it to another thread. To share unsynchronized memory with unsafe
you, well, have to use unsafe
.
This ties into the next point:
Difference: Thread-safety tracking
Rust's type system tracks some notion of thread safety. Specifically, the Sync
trait denotes types that can be shared by several threads without risk of data races, while Send
marks those that can be moved from one thread to another. This is enforced by the compiler throughout the program, and thus library designers dare make optimizations that would be stupidly dangerous without these static checks. For example, C++'s std::shared_ptr
which always uses atomic operations to manipulate its reference count, to avoid UB if a shared_ptr
happens to be used by several threads. Rust has Rc
and Arc
, which differ only in that Rc
uses non-atomic refcount operations and isn't threadsafe (i.e. doesn't implement Sync
or Send
) while Arc
is very much like shared_ptr
(and implements both traits).
Note that if a type doesn't use unsafe
to manually implement synchronization, the presence or absence of the traits are inferred correctly.
Difference: Very strict rules
If the compiler cannot be absolutely sure that some code is free from data races and other UB, it will not compile, period. The aforementioned rules and other tools can get you quite far, but sooner or later you will want to do something that's correct, but for subtle reasons that escape the compiler's notice. It could be a tricky lock-free data structure, but it could also be something as mundane as "I write to random locations in a shared array but the indices are computed such that every location is written to by only one thread".
At that point you can either bite the bullet and add a bit of unnecessary synchronization, or you reword the code such that the compiler can see its correctness (often doable, sometimes quite hard, occasionally impossible), or you drop into unsafe
code. Still, it's extra mental overhead, and Rust does not give you any guarantees for the correctness of the unsafe
code.
Difference: Fewer tools
Because of the aforementioned differences, in Rust it's much more rare that one writes code that may have a data race (or a use after free, or a double free, or ...).
While this is nice, it has the unfortunate side effect that the ecosystem for tracking down such errors is even more underdeveloped than one would expect given the youth and small size of the community.
While tools like valgrind and LLVM's thread sanitizer could in principle be applied to Rust code, whether this actually works yet varies from tool to tool (and even those that work may be hard to set up, especially since you may not find any up-to-date resources on how to do it).
It doesn't really help that Rust currently lacks a real specification and in particular a formal memory model.
In short, writing unsafe
Rust code correctly is harder than writing C++ code correctly, despite both languages being roughly comparable in terms of capabilities and risks. Of course this must be weighted against the fact that a typical Rust program will contain only a relatively small fraction of unsafe
code, whereas a C++ program is, well, fully C++.
Best Answer
You need to explicitly implement the trait; happening to have a method with matching name/signature is meaningless for Rust.
Generic call dispatching
Not providing static dispatch can be a significant performance hit for certain cases (e.g. the
Iterator
one I mention below). I think this is what you mean bybut I'll cover it in more detail, because it's worth understanding the difference deeply.
In Rust
Rust's approach allows for the user to choose between static dispatch and dynamic dispatch. As an example, if you have
then the two
call_bar
calls above will compile to calls to, respectively,where those
.bar()
method calls are static function calls, i.e. to a fixed function address in memory. This allows for optimisations like inlining, because the compiler knows exactly which function is being called. (This is what C++ does too, sometimes called "monomorphisation".)In Go
Go only allows dynamic dispatch for "generic" functions, that is, the method address is loaded from the value and then called from there, so the exact function is only known at runtime. Using the example above
Now, those two
call_bar
s will always be calling the abovecall_bar
, with the address ofbar
loaded from the interface's vtable.Low-level
To rephrase the above, in C notation. Rust's version creates
For Go, it's something more like:
(This isn't exactly right---there has to be more information in the vtable---but the method call being a dynamic function pointer is the relevant thing here.)
Rust offers the choice
Going back to
So far I've only demonstrated Rust having statically dispatched generics, but Rust can opt-in to the dynamic ones like Go (with essentially the same implementation), via trait objects. Notated like
&Foo
, which is a borrowed reference to an unknown type that implements theFoo
trait. These values have the same/very similar vtable representation to the Go interface object. (A trait object is an example of an "existential type".)There are instances where dynamic dispatch is really helpful (and sometimes more performant, by, e.g. reducing code bloat/duplication), but static dispatch allows compilers to inline the callsites and apply all their optimisations, meaning it is normally faster. This is especially important for things like Rust's iteration protocol, where static dispatching trait method calls allows for those iterators to be as fast as the C equivalents, while still seeming high-level and expressive.
Tl;dr: Rust's approach offers both static and dynamic dispatch in generics, at the programmers discretion; Go only allows for dynamic dispatch.
Parametric polymorphism
Furthermore, emphasising traits and deemphasising reflection gives Rust much stronger parametric polymorphism: the programmer knows exactly what a function can do with its arguments, because it has to declare the traits the generic types implement in the function signature.
Go's approach is very flexible, but has fewer guarantees for the callers (making it somewhat harder for the programmer to reason about), because the internals of a function can (and do) query for additional type information (there was a bug in the Go standard library where, iirc, a function taking a writer would use reflection to call
Flush
on some inputs, but not others).Building abstractions
This is somewhat of a sore point, so I'll only talk briefly, but having "proper" generics like Rust has allows for low level data types like Go's
map
and[]
to actually be implemented directly in the standard library in a strongly typesafe way, and written in Rust (HashMap
andVec
respectively).And its not just those types, you can build type-safe generic structures on top of them, e.g.
LruCache
is a generic caching layer on top of a hashmap. This means people can just use the data structures directly from the standard library, without having to store data asinterface{}
and use type assertions when inserting/extracting. That is, if you have anLruCache<int, String>
, you're guaranteed that the keys are alwaysint
s and the values are alwaysString
s: there's no way to accidentally insert the wrong value (or try to extract a non-String
).