Tour of Rust’s Standard Library Traits github.com/pretzelhamm… Praying for Rust
Contents ✅ ⏰
-
The introduction ✅
-
Trait based ✅
-
Automatic Trait ⏰ = > ✅
-
Generic Trait ⏰
-
Formatting Trait ⏰
-
Operator Trait ⏰
-
Conversion Trait ⏰
-
Error handling ⏰
-
The iterator traits ⏰
-
The I/O Trait ⏰
-
Conclusion ⏰
Automatic Trait
Send & Sync
Required knowledge
- Marker Traits
- Auto Traits
- An Unsafe Trait.
unsafe auto trait Send {}
unsafe auto trait Sync {}
Copy the code
If a type is Send, it means that it can be safely sent between threads. If a type is Sync, this means it can safely share references between threads. To be more precise, type T is Sync if and only if &T is Send.
Almost all of them are Send and Sync. The only notable Send exception is Rc, and the Sync exception is Rc, Cell, and RefCell. If we need an Rc that satisfies Send, we can use Arc. If we need a Sync version of the Cell or RefCell, we can use Mutex or RwLock. Although we use Mutex and RwLock to wrap a primitive type, it is generally better to use the atomic types provided by the standard library, such as AtomicBool, AtomicI32, AtomicUsize, and so on.
It may come as a surprise to some that almost all types are Sync, but it’s true even for types that don’t have any internal synchronization. This is possible thanks to Rust’s strict borrowing rules.
We can pass multiple immutable references to the same piece of data into multiple threads, and since Rust statically guarantees that the underlying data will not be modified as long as immutable references exist, we can guarantee that data contention will not occur.
use crossbeam::thread;
fn main() {
let mut greeting = String::from("Hello");
let greeting_ref = &greeting;
thread::scope(|scoped_thread| {
// spawn 3 threads
for n in 1..=3 {
// greeting_ref copied into every thread
scoped_thread.spawn(move| _ | {println!("{} {}", greeting_ref, n); // prints "Hello {n}"
});
}
// line below could cause UB or data races but compiler rejects it
greeting += " world"; ❌ cannot mutate greeting while immutable refs exist
});
// can mutate greeting after every thread has joined
greeting += " world"; / / ✅
println!("{}", greeting); // prints "Hello world"
}
Copy the code
Similarly, we can pass a mutable reference to data to a separate thread, and since Rust statically guarantees that there is no mutable reference alias, the underlying data will not be modified by another mutable reference, so we can also guarantee that data contention will not occur.
use crossbeam::thread;
fn main() {
let mut greeting = String::from("Hello");
let greeting_ref = &mut greeting;
thread::scope(|scoped_thread| {
// greeting_ref moved into thread
scoped_thread.spawn(move |_| {
*greeting_ref += " world";
println!("{}", greeting_ref); // prints "Hello world"
});
// line below could cause UB or data races but compiler rejects it
greeting += "!!!"; ❌ cannot mutate greeting while mutable refs exist
});
// can mutate greeting after the thread has joined
greeting += "!!!"; / / ✅
println!("{}", greeting); // prints "Hello world!!!"
}
Copy the code
This is why most types satisfy Sync without requiring any explicit synchronization. When we need to modify some data T simultaneously in multiple threads, the compiler will not allow us to do so unless we wrap the data in Arc
> or Arc
>, so the compiler will force explicit synchronization if necessary.
Sized
Preliminary knowledge:
- Marker Traits
- Auto Traits
If a type is Sized, this means that its type size is known at compile time and an instance of the type can be created on the stack.
The size and meaning of types is a subtle and huge topic that affects many aspects of a programming language. Because it’s so important, I wrote a separate article called Sizedness in Rust, which I highly recommend for anyone who wants to learn more about Sizedness. I will summarize the key points of this article below.
- All generic types have an implicit
Sized
Constraints.
fn func<T>(t: &T) {}
// example above desugared
fn func<T: Sized>(t: &T) {}
Copy the code
- Because all generic types have an implicit
Sized
Constraint. If we want to opt out of this constraint, we need to use the specific relaxed bound syntax —? Sized
, the syntax is currently only forSized
Trait.
// now T can be unsized
fn func<T: ?Sized>(t: &T) {}
Copy the code
- All traits have an implicit one
? Sized
Constraints.
trait Trait {}
// example above desugared
trait Trait:?Sized {}
Copy the code
This is to enable trait objects to implement traits. Again, all the nitty-gritty is in Sizedness in Rust.