Lecture 14 Rust
Lecture 14 Rust
Lecture 14
Rust
1
Principles of Concurrency
Motivation
2
Sustainability
Can I look at my existing service
dashboards with infrastructure costs, performance, memory, etc and use the trends I see to infer something about the
trends in my service’s energy consumption?
What the study did is implement 10 benchmark problems in 27 different programming languages and measure execution
4 energy consumption, and peak memory use. C and Rust significantly outperformed other languages in energy
time,
efficiency. In fact, they were roughly 50% more efficient than Java and 98% more efficient than Python.
Discord started as a mostly Python, Go, and Elixir shop, and they had a problem with one of their key Go services. It was a
pretty simple service, but it had slow tail latencies. Because Go is a garbage collection (GC) language, as objects are
created and released, every so often, the garbage collector needs to stop execution of the program and run a garbage
collection pass. While the GC is running, the process is unable to respond to requests, and you can see the spikes on the
CPU and response time graphs when it’s running.
Go Rust
It’s not a surprise that C and Rust are more efficient than other languages. What is shocking is the magnitude of the
difference. Broad adoption of C and Rust could reduce energy consumption of compute by 50% – even with a
conservative estimate.
Torvalds called Rust the first language he’s seen that might actually be a solution. Rust delivers the energy efficiency of C
without the risk of undefined behavior. We can cut energy use in half without losing the benefits of memory safety.
Several analyses have concluded that more than 70% of the high severity CVEs that occur in C/C++ would be prevented
by implementing those same solutions in Rust. In fact, the Internet Security Research Group (ISRG), the nonprofit that
supports the Let’s Encrypt project, the Certificate Authority for 260 million websites,To
hasfix the issue, Discord decided to try rewriting the service in Rust, and these are the results. The Go implementation is
Discord server
a goal to move all internet
on thesupport
security sensitive infrastructure to memory safe languages. The projects underway include left and
forthe Rust
Rust implementation
in the Linux is on the right. While the GC spike pattern is gone on the Rust graph, the really
Principles of Concurrency, Spring 2022
kernel and migrating curl to Rust implementations of TLS and HTTP. amazing difference is the magnitude of the change. The Go and Rust graphs are actually using different units.
The Problem Being Addressed
Instead of using a garbage collector to maintain safety, Rust uses ownership and borrow checking. Ownership is fairly
simple but has deep implications for the rest of the Rust programming language. In Rust, all memory is owned by a
single variable. That variable is called its owner. There can be only one owner at a time, but ownership of the data can be
5
passed around.
First, here is an example of message passing with Go. On the left side, we create a gift, then send it via the channel.
some other go routine on the right side, the gift is received and opened. The Go’s garbage collector is going to mana
the memory for us. However, in the code on the left side, we accidentally opened the gift after sending it into the
In Go
channel. The gift is going to be opened twice, resulting in a bug.
First, here is an example of message passing with Go. On the left side, we create a gift, then send it via the channel. On
some other go routine on the right side, the gift is received and opened. The Go’s garbage collector is going to manage
the memory for us. However, in the code on the left side, we accidentally opened the gift after sending it into the
channel. The gift is going to be opened twice, resulting in a bug.
contributed articles
In Rust Data Race
Figure 1. Use-after-free bug in C++ and how the bug is prevented in Rust. behind several layers
Here is the same message passing example with Rust. The gift is created and assigned. We say that the `gift`
particular when variab
code
owns the data. Ownership of the gift is passed into the channel. The channel consumer receives new features
the gift, get add
taking
}
1 std::vector<int> v { 10, 11 }; impossible to determ
ownership, and is able
2 intto *vptr
open it. If we try
= &v[1]; // to open*into*
Points the gift
v. after sending it into the channel, the compiler will shout
3 v.push_back(12);
C++ certain vector will i
because we are violating the ownership rules. Already, we
4 std::cout << *vptr; // Bug (use-after-free)
are seeing how Rust helps us prevent bugs.
elsewhere in the pro
v ing to be used again l
Here is the same message passing example with Rust. The gift is created and assigned. We say that the `gift` variable with
Comparison
owns the data. Ownership of the gift is passed into the channel. The channel consumer receives the gift, taking Languag
languages.
10 10
and will
ownership, and is able to open it. If we try to open the gift after sending it into the channel, the compiler OCaml
shoutavoid
at us, u
vptr 11 11
because we are violating the ownership rules. Already,12we are seeing how Rust helps us prevent bugs. using garbage colle
only deallocated whe
Use-after-free
}
1 let mut v = vec![10, 11]; be used by the prog
2 let vptr = &mut v[1]; // Points *into* 'v'. can be no dangling
Rust
3 v.push(12);
Because Rust enforces the rule that only
4 println!("{}", one//variable
*vptr); owns
Compiler data, when that variable goes out of use-after-free.
error scope without pass
Principles of Concurrency, Spring 2022 One problem wit
off ownership, there is no possible way for the data to be accessed. Rust takes advantage of that and will automatica
tion is that, to make
asing is
sing is to worry,
to worry, for
for instance,
instance, about
aboutclosing duceanother
closing duce anotherconcept:
concept:lifetimes.
lifetimes.Just
Just
Key Innovations: Ownership and Borrowing
ays has
ays
ated
has
by
ted by
files or
files orreleasing
releasinglocks.
locks.Using
Usingdestruc- likeininreal
destruc- like reallife,
life,when
when borrowing
borrowing some-
some-
6
Figure2.2.Rust
Figure Rustexample:
example:Moving
Movingownership.
ownership.
to
to our
our
oo con-
con- 11 fn
fn consume(w:
consume(w: Vec<i32>)
Vec<i32>){ { Could have been automatically
nn C++,
C++, 22 drop(w);
drop(w); //// deallocate
deallocatevector
vector inserted by the compiler
but 33 }}
but the
the
44 let
let vv == vec![10,
vec![10,11];
11];
copied
copied 55 consume(v);
consume(v);
uplicat-
uplicat- 66 v.push(12);
v.push(12); // // Compiler
Compilererror
error Ownership
ttto
tothe
the
pass-by-value
vv and
andww
m, but Figure
Figure3.3.Rust
Rustexample:
example:Mutable
Mutablereferences.
references.
am, but
ds to
ads to aa
ecause
because
o have
to have
11 fn
fn add_something(v:
add_something(v:&mut &mutVec<i32>)
Vec<i32>){ { track lifetimes
22 v.push(11);
he
he call,
call, 33 }}
v.push(11);
whatev-
whatev- 44 let
let mut
mut vv == vec![10];
vec![10];
er
er may
may 5
5
add_something(&mut v);
add_something(&mut v);
acking 6 v.push(12); // Ok!
backing 6 v.push(12); // Ok!
7 // v.push(12) is syntactic sugar for Vec::push(&mut v, 12)
7 // v.push(12) is syntactic sugar for Vec::push(&mut v, 12)
nership
nership Borrowing
memory
memory pass-by-reference
Principles of Concurrency, Spring 2022
Ownership
What about using a GC? (like Haskell, Java, Go, . . . ) What about using a GC? (like Haskell, Java, Go, . . . )
Garbage collection: Garbage collection:
The programmer allocates vectors, strings, etc. The programmer allocates vectors, strings, etc.
The runtime system periodically sweeps through memory, looks The runtime system periodically sweeps through memory, looks
7 for unreferenced data and deallocates it. for unreferenced data and deallocates it.
8 Loss of control 8 Loss of control
8 Runtime overhead
Ownership
8 Doesn’t help with other safety issues: iterator invalidation, data
8 Runtime overhead
Move S
8 Doesn’t help with other safety issues: iterator invalidation, data
races, etc. races, etc.
So, what is Rust’s solution? Ownership So, what is Rust’s solution? Ownership (affine linear typing)
A variable binding takes ownership of its data. [lifetimes]
A piece of data can only have one owner at a time. let v1 =
When a binding goes out of scope, the bound data is released let v2 =
println!
automatically.
For heap-allocated data, this means de-allocation. let v
Data must be guaranteed to outlive its references.
CIS 352 Rust Overview 13 / 1 CIS 352 Rust Overview 13 / 1
W
T
fn foo() { S
// Creates a Vec object.
Observation from C++-land Three Basic Patterns print
// Gives ownership of the Vec object to v1.
let mut v1 = vec![1, 2, 3]; W
v1.pop(); R
v1.push(4); M
// At the end of the scope, v1 goes out of scope. It
// v1 still owns the Vec object, so it Ownership fn foo(v: T) { ...}
can be cleaned up.
} Shared Borrow fn foo(v: &T) { ...}
Mutable Borrow fn foo(v: &mut T) { ...}
CIS 352 Rust Overview 16 / 1
Move Semantics
What about using
Fora heap-allocated
GC? (like Haskell, Java,
data, this Go,de-allocation.
means ...) What about using a GC? (like Haskell,
let v2 Java, Go, .
= v1;
Data
Garbage collection: must be guaranteed to outlive its references. Garbage collection: We don’t wan
The programmer allocates vectors, strings, etc. The data cann
The programmer allocates vectors, strings, etc.
8
Move Semantics
The runtime system periodically
fn foo() { sweeps through memory, looks The runtime system periodically sweeps through memory,
Solution: molo
for unreferenced
// Creates a Vec it.object.
data and deallocates for unreferenced data and deallocates it.
8 Loss of control // Gives ownership of the Vec object to v1. 8 Loss of control println!("{}",
[lifetimes] 8 Runtime overhead let mut v1 = vec![1, 2, 3]; 8 Runtime overhead We know tha
8 Doesn’t help withv1.pop();
other safety issues: iterator invalidation, data 8 Doesn’t help with other safety issues: iteratorRust
invalidation,
can reasd
races, etc.
let v1 = vec![1, 2, 3]; races, etc.
v1.push(4); Moving own
eleased let v2
So, what is Rust’s solution? = v1;
Ownership // Ownership of the Vec object
So, moves
what is tosolution?
Rust’s v2. Ownership (affine linear typing) inv
// At the end of the scope, v1 goes out of scope. It doesn’t
println!("", v1[2]); // error: use of moved value ‘v1‘
// v1 still owns the Vec object, so it can be cleaned up.
}
let v2 = v1;
We don’t want to copy the data, since that’s expensive.
The data cannot haveCISmultiple
352 Rust Overview 16 / 1
owners.
Solution: move the Vec’s ownership into v2, and declare v1 invalid.
println!("{}", v1[2]);
CIS 352 Rust Overview 13 / 1 CIS 352 Rust Overview 13 / 1
Ownership does
We know thatnot
v1 isalways
no longerhave
a validto be moved
variable binding, ) error! Borrowing
Observation from C++-land ) compiler
Rust can reason about this at compile time,Three error.
Basic Patterns
Moving ownership is a compile-time semantic.
It doesn’t involve moving data during your program. In place of transf
aned up.
Rust would be a pain to write if we were forced to explicitly move A variable’s data
ownership back and forth. variable (i.e., alia
10 Borrowing
The runtime system periodically sweeps through memory, looks
for unreferenced data and deallocates it. Borrowi
The runtime system periodically sweeps through memory, looks
for unreferenced data and deallocates it.
8 Loss of control 8 Loss of control
8 Runtime overhead 8 Runtime overhead
8 Doesn’t help with//other safety issues:
‘length‘ onlyiterator invalidation,
needs ‘vector‘data temporarily,
8 Doesn’tsohelpitwith
is other // ‘push‘
safety issues: iterator invalidation,
borrowed. data
races, etc. races, etc. fn push(ve
fn length(vec_ref: &Vec<i32>) -> usize {
So, what is Rust’s solution? Ownership So, what is Rust’s solution? Ownership (affine linear typing) vec ref
// vec_ref is auto-dereferenced when you call methods on it.
vec_ref.len() }
} fn main()
fn main() { let mu
let vector = vec![]; let ve
length(&vector); push(v
println!("{:?}", vector); // this is fine }
CIS 352 Rust Overview 13 / 1 CIS 352 Rust Overview 13 / 1
} Variab
References, like bindings, are immutable byBasic
default. ve
Observation from C++-land Three Patterns
Th
The borrow is over after the reference goes out of scope (at the
Differe
end of length).
You ca
(usize =The pointer-sized unsigned integer type.)
Also y
Ownership fn foo(v: T) { ...}
Shared Borrow fn foo(v: &T) { ...}
CIS 352 Rust Overview 20 / 1
Mutable Borrow fn foo(v: &mut T) { ...}
Rustis
vec ref a reference
CIS 352
Overview 20 / 1 to a mutable Vec. CIS 352 Ru
Observation from C++-landThe type is &mut Vec<i32>, not &Vec<i32>.Three Basic Patterns
ope (at the
Different from a reference which is variable.
Borrowing Rules You can have exactly one mutable borrow at a time. Borrowing Prevents: Use-afte
Also you cannot dereference borrows (changes ownership).
What about using a GC? (like Haskell, Java, Go, . . . ) What about using a GC? (like Haskell, Java, Go, . . . )
ules Consequences
Garbage collection:
The programmer allocates vectors, strings, etc.
The runtime system periodically sweeps through memory, looks
Borrowing Prevents:
Garbage collection:
The Use-after-free bugsstrings, etc.
programmer allocates vectors,
The runtime system periodically sweeps through memory, looks
12 for unreferenced data and deallocates it. for unreferenced data and deallocates it.
8 Loss of control 8 Loss of control
8 Runtime overhead 8 Runtime overhead
8 Doesn’t help with other safety issues: iterator invalidation, data 8 Doesn’t help with other safety issues: iterator invalidation, data
races, etc. Valid in C, C++,. . . races, etc.
So, what is Rust’s solution? Ownership let y: &i32; So, what is Rust’s solution? Ownership (affine linear typing)
p borrowing something after it stops existing. {
ay have many immutable references to it (&T). let x = 5;
y = &x; // error: ‘x‘ does not live long enough
ne mutable reference (&mut T) (not both). }
println!("{}", *y);
The ownership rules also prevent other things Concurrency
This eliminates vast numbers of memory safety bugs at compile time!
Under the standard ownership CIS 352
rules:
Rust Overview 13 / 1 CIS 352 Rust Overview 13 / 1
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
use std::thread;
fn main() {
let v = vec![1, 2, 3];
Why doesn’t this compile?
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
fn main() {
int main() {
let mut msg = “Hello”.to_string();
std::string msg = “Hello”;
let handle = thread::spawn(|| {
std::thread t1([&]() {
println!(“{}, &msg);
std::cout << msg << std::endl;
});
});
msg.push_str(“,world”);
msg += “,world”;
handle.join().unwrap();
t1.join();
}
return 0;
}
Rust generates two errors:
- cannot borrow ‘msg’ as mutable; why?
- msg does not live long enough? why?
To x these errors - modify msg and then invoke the thread
Principles of Concurrency, Spring 2022
fi
Concurrency
16
use std::thread;
Move gives ownership to the spawned thread; the
fn main() { parent no longer has access to v
let v = vec![1, 2, 3];
handle.join().unwrap();
}
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
Why does this not
compile? *num += 1;
});
handles.push(handle);
}
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}