What Makes Rust Super Star¶
Rust is a system programming language that runs blazingly fast, prevent segfaults, and guarantees thread safety.
Low-level system programming, such as C, C++, Assembly, had developed for nearly 50 years. But, there are two thorny problems that have not completely solved.
It’s hard to write memory safety code
It’s hard to write thread safety code
How about break down these traditional programming language barriers and design an absolutely new modern language?
Why not?
That’s what rust does. Rust empowers you to build more reliable and efficient software.
How rust achieve¶
Rust design principles:
Memory Security
Zero Cost Abstraction
Thread Safety
Memory Security¶
Different programming languages deal with memory issues in different ways. Some use GC mechanism and others like C/C++ completely hand over this complex task to the developers. How rust achieve memory safety guarantees without a garbage collector?
Rust memory management tools:
Ownership.
References and lifetime.
Let’s take about ownership firstly.
Ownership rules:
Each value in Rust has a variable that’s called its owner
There can only be one owner at a time
When the owner goes out of scope, the value will be dropped
Demo1 - Normal Reference
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
When s
goes out of scrope, rust call a special function named drop
, put the code to return the memory.
Demo2 - Invalid Reference
let s1 = String::from("hello"); // Actually, s1 is a reference
let s2 = s1; // assign s1 to s2
println!("{}, world!", s1);
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |
4 | let s2 = s1;
| -- value moved here
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
The compiler says you can not use s1
anymore since you have moved ownership of data under s1
to s2
.
You know String
type is made up of three parts(a pointer to the memory that hold the content of the string, a length and a capacity).
When assigning s1
to s2
, it only copy the pointer and do not copy the data under the pointer.
Compiler helps you found this potential memory security tricky issue at compile time.
Demo3 - Dangling Reference
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // Return a reference
let s = String::from("hello");
&s // Return a reference which point to s
} // Here, s goes out of scope and dropped. So where does s point now?
Unexpectedly, compiler says no.
Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ help: consider giving it a 'static lifetime: `&'static`
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: Could not compile `playground`.
Diving into the error message, it’s clearly:
this function's return type contains a borrowed value, but there is no value for it to be borrowed from
Zero Cost Abstraction¶
The core concept of the rust zero cost abstraction is Trait System
. In rust, trait is the abstraction of type
behavior. Similar to interface in other languages, but more abstracted and powerful.
Demo1 - Trait Simplest Usage
I. Define a Trait
pub trait Summary { // Defining a Trait `Summary`
fn summarize(&self) -> String; // define a behavior named summarize
}
II. Implementing a Trait on a type
pub struct NewsArticle { // Define a struct `NewsArticle`
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle { // Implement `Summary` for `NewsArticle`
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet { // Another struct `Tweet`
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet { // Implement `Summary` for `Tweet`
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
III. Trait as Parameters
pub fn notify(item: impl Summary) { // This parameter accepts any type that implements trait `Summary`
println!("Breaking news! {}", item.summarize());
}
IV. Multiple trait bounds
pub fn notify<T: Summary + Display>(item: T) { // item must implement both Display and Summary
// Display is another trait, like Summary.
println!("Breaking news! {}", item.summarize());
}
In addtion, rust provides several builtin traits:
Sized trait: The trait indicates the type size is known at compile time
Copy trait: The trait indicates the type can be copied in bits
Send trait: The trait indicates the ownership of the type can be transferred between threads
Sync trait: The trait indicates it is safey for the type can be referenced from multiple threads
Thread Safety¶
Because of the uncontrolled system level threads, the code we write not works as we expected in multiple threads.
To achieve thread safety, rust provides two killer traits named Send
and Sync
:
Send trait: Allowing transference of ownership between threads
Sync trait: Allowing access from multiple threads
Demo1 - Sharing Immutable Variable Between Threads
use std::thread;
fn main() {
let x = vec![1, 2, 3, 4];
thread::spawn(|| x);
}
That’s ok.
Demo2 - Sharing Mutable Variable Between Threads
use std::thread;
fn main() {
let mut x = vec![1, 2, 3, 4]; // New a mutable vector
thread::spawn(||{ // Try to modify value of vector in child
x.push(1);
});
x.push(2); // Modify value of vector in main to simulate data race
}
Compiler says no.
Compiling playground v0.0.1 (/playground)
error[E0373]: closure may outlive the current function, but it borrows `x`, which is owned by the current function
--> src/main.rs:5:19
|
5 | thread::spawn(||{
| ^^ may outlive borrowed value `x`
6 | x.push(1);
| - `x` is borrowed here
|
The x
in closure is a reference to x
in main thread. The rust compiler can not determine which x
lives longer.
If x
in the main thread dropped, then the closure x
becomes a dangerous dangling reference.
Next, you can use move
keyword to transfer ownership of x
from main to child as following:
use std::thread;
fn main() {
let mut x = vec![1, 2, 3, 4]; // New a mutable vector
thread::spawn(move ||{ // Use move to transfer x's ownership from main thread to child thread.
// Thus, x can be accessed in child thread
x.push(1);
});
// x.push(2); // The main thread can not modify x, because x's ownership had transferred to child thread.
}
Note: A variable can use move
to transfer ownership only when the type has implemented Send
and Sync
traits.
By default, almost all primitive types are Send
, any type composed entirely of Send
types is automatically marked as Send
as well.
Demo3 - Transfer Ownership Between Threads
use std::thread;
use std::rc::Rc;
fn main() {
let mut x = Rc::new(vec![1, 2, 3, 4]); // New a mutable vector and wrap it with Rc container.
thread::spawn(move ||{ // Try to move ownership of Rc container to child thread.
x.push(1);
});
}
Rc<T>
is a smart pointer provided by rust used to enable multiple owners to a value. Rc<T>
is not a Send
and Sync
type and not safe to share across threads. In other words, you can’t use move
to transfer ownership of Rc container. Compiler will say no!
Compiling playground v0.0.1 (/playground)
error[E0277]: `std::rc::Rc<std::vec::Vec<i32>>` cannot be sent between threads safely
--> src/main.rs:6:5
|
6 | thread::spawn(move ||{
| ^^^^^^^^^^^^^ `std::rc::Rc<std::vec::Vec<i32>>` cannot be sent between threads safely
|
= help: within `[closure@src/main.rs:6:19: 8:6 x:std::rc::Rc<std::vec::Vec<i32>>]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<std::vec::Vec<i32>>`
= note: required because it appears within the type `[closure@src/main.rs:6:19: 8:6 x:std::rc::Rc<std::vec::Vec<i32>>]`
= note: required by `std::thread::spawn`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: Could not compile `playground`.
In order to share variable across threads, rust provides another safecontainer named Arc<T>
.
Arc
is a smart pointer like Rc
, but it implemented Send
and Sync
. It’s movable.
Demo4 - Sharing Immutable Variable Between Threads
use std::thread;
use std::sync::Arc;
fn main() {
let x = Arc::new(vec![1, 2, 3, 4]); // New a mutable vector and wrap it with Arc container.
thread::spawn(move ||{ // Try to move ownership of Rc container to child thread.
});
println!("vector is {:?}", x);
}
Compiler says yes!
Note: We changed x
in main thread as immutable. The data wraped in Arc
is read-only.
If you want to modify data inside, Mutex<T>
is needed.
Demo5 - Sharing Mutable Between Threads
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let x = Arc::new(Mutex::new(vec![1, 2, 3, 4])); // New a mutable vector and wrap it with Arc<Mutex<>> container.
let x_clone = x.clone(); // Clone means creating a reference to data inside container
let child = thread::spawn(move ||{ // Try to modify vector in child thread
let mut x1 = x_clone.lock().unwrap();
x1.push(5);
});
let _ = child.join(); // call join, let main thread wait for child thread to finish
let mut x2 = x.lock().unwrap(); // Modify vector in main thread
x2.push(6);
println!("vector is {:?}", x2);
}
Mutex<T>
is a container for safety sharing mutable variable across threads. a mutex allows only one thread to access data at any time.
To access the data inside the mutex, we use the lock
method to acquire the lock.
Output:
vector is [1, 2, 3, 4, 5, 6]
Demos above demonstrate how rust achieve thread safety.