Felix Klock (@pnkfelix
), Mozilla
space: next slide; shift space: prev slide; esc: overview; arrows navigate http://bit.ly/2rM9w0r
No segmentation faults
No undefined behavior
No data races
msg passing via channels
shared state (R/W-capabilities controlled via types)
use native threads... or scoped threads... or work-stealing...
enum BinaryTree { Leaf(i32), Node(Box<BinaryTree>, i32, Box<BinaryTree>) }
fn sample_tree() -> BinaryTree {
let l1 = Box::new(BinaryTree::Leaf(1));
let l3 = Box::new(BinaryTree::Leaf(3));
let n2 = Box::new(BinaryTree::Node(l1, 2, l3));
let l5 = Box::new(BinaryTree::Leaf(5));
BinaryTree::Node(n2, 4, l5) }
enum
declares an algebraic (sum-of-product) data typeBox<T>
is an owning reference to heap-allocated data of type T
enum BinaryTree { Leaf(i32), Node(Box<BinaryTree>, i32, Box<BinaryTree>) }
match
moves input to Leaf(n)
or Node(l, n, r)
as appropriate*expr
dereferences; here, moves tree out of box to local stack slotfn
signature: pass-by-value + move semantics = args consumedfn tree_weight(t: BinaryTree) -> i32 {
match t {
BinaryTree::Leaf(payload) => payload,
BinaryTree::Node(left, payload, right) => {
tree_weight(*left) + payload + tree_weight(*right)
}
}
}
fn tree_weight(t: BinaryTree) -> i32 { ... }
fn consume_semantics() {
let t = sample_tree();
let w1 = tree_weight(t);
let w2 = tree_weight(t);
println!("w1: {} w2: {}", w1, w2);
}
error[E0382]: use of moved value: `t`
--> src/a00.rs:67:17
|
66 | tree_weight(t);
| - value moved here
67 | tree_weight(t);
| ^ value used here after move
|
= note: move occurs because `t` has type `BinaryTree`,
which does not implement the `Copy` trait
std::thread
)let (tx, rx) = channel(); // create transmitter/receiver pair
thread::spawn(move || match rx.recv() { // spawn A1, taking ownership of `rx`
Ok(v_end) => {
println!("A1 received {:?}", v_end)
} // (end of scope for `v_end`; automatic clean up.)
Err(err) => println!("A1 receive failure {:?}", err),
});
let t1 = tx.clone();
thread::spawn(move || { // spawn A2, taking ownership of `t1`
let mut v = Vec::new(); v.push('a'); v.push('b'); v.push('c');
t1.send(v).expect("A2 send failure");
});
thread::spawn(move || { // spawn A3, taking ownership of `tx`
let v = vec!['1', '2', '3'];
tx.send(v).expect("A3 send failure");
});
|arg, ...| expr
(incl. || { expr; ... }
) are closuresmove |arg, ...| expr
captures by ownership xfer ("moving")println!
and vec!
are macros (typesafe printf; Vec building sugar)v_end
, buffer + contents reclaimedrx
, t1
+ tx
, channel reclaimedrx
, t1
, tx
, and each v
are all moved from one thread to anothert1
cloned from tx
; (if tx
moved to middle subthread, would not be available for left one)fn demo_sendable() {
let (tx, rx) = channel();
let r1 = Rc::new(vec!['a', 'b', 'c', '1', '2', '3']);
tx.send(r1.clone()).unwrap();
let _r2 = rx.recv().unwrap();
println!("r1: {:?}", r1);
}
(above is fine)
Rc<T>
is a reference-counted pointer to a heap-allocated T
Box<T>
, but with dynamically-tracked shared ownership rather than statically-tracked sole ownership)fn demo_unsendable() {
let (tx, rx) = channel();
let r1 = Rc::new(vec!['a', 'b', 'c', '1', '2', '3']);
thread::spawn(move || { let r2 = rx.recv().unwrap();
println!("received: {:?}", r2); });
tx.send(r1.clone()).unwrap();
println!("r1: {:?}", r1);
}
error[E0277]: the trait bound `Rc<Vec<char>>: Send` is not satisfied
--> src/a00.rs:405:5
|
405 | thread::spawn(move || {
| ^^^^^^^^^^^^^ `Rc<Vec<char>>` cannot be sent between threads safely
|
= help: the trait `Send` is not implemented for `Rc<Vec<char>>`
= note: required because of the requirements on the impl of `Send`
for `Receiver<Rc<Vec<char>>>`
= note: required because it appears within the type
`[closure@src/a00.rs:405:19: 410:6 Receiver<Rc<Vec<char>>>]`
= note: required by `std::thread::spawn`
Will revisit these constraints a bit more later
Digression: talk about sharing for a moment
We can solve any problem by introducing an extra level of indirection
-- David J. Wheeler
We need references; allows distinguishing:
Ownership enables: | which removes: |
---|---|
RAII-style destructors | source of memory leaks (or fd leaks, etc) |
no dangling pointers | many resource management bugs |
no data races | many multithreading heisenbugs |
Do I need to take ownership here, accepting the associated resource management responsibility? Would temporary access suffice?
Good developers ask this already!
Rust forces function signatures to encode the answers
(and they are checked by the compiler)
Pointers: Perhaps the Pandora's Box of Computer Science
&mut T
, &T
Box<T>
, Cow<T>
, Rc<T>
, ...)T
above ranges over both so-called "sized" and "unsized" types&char
, &mut Vec<u8>
, &[i32; 16]
&str
, &mut [u8]
, &Fn() -> i32
&T
, Box<T>
, Rc<T>
etcT
: owned instance of T
, stored inline (e.g. in stack frame or record)Box<T>
: owned instance of T
, stored on heap (so Box<T>
itself is just a pointer)&T
: pointer to T
, but not owned. Extent is limited to some static scope (possibly a scope known only to function's caller).Rc<T>
: ref-counted pointer to T
; shared ownership. (At end of scope for our Rc<T>
, we might be responsible for resource cleanup.)&
-reference types?Distinguish exclusive access from shared access
Enables safe, parallel API's
Ownership | T |
|
Exclusive access | &mut T |
("mutable") |
Shared access | &T |
("read-only") |
A &T
provides limited access; cannot call methods that require ownership or exclusive access to T
A &mut T
provides temporary exclusive access; even the original owner cannot mutate the object while you have the reference
But cannot claim ownership of T
yourself via &mut T
, unless you swap in another T
to replace one you take (Rust coding pattern)
fn demo_references() {
let v = vec!['a', 'b', 'c', '1', '2', '3'];
let ref1 = &v[0..3];
let ref2 = &v[3..6];
let f1 = |i| println!("ref1[{}]: {:?}", i, ref1[i]);
let f2 = |i| println!("ref2[{}]: {:?}", i, ref2[i]);
f1(1);
f2(2);
}
Rc<T>
(shared ownership)fn demo_refcounts() {
let r1 = Rc::new(vec!['a', 'b', 'c', '1', '2', '3']);
let r2 = r1.clone();
let f1 = |i| println!("v[{}]: {:?}", i, r1[i]);
let f2 = |i| println!("v[{}]: {:?}", i, r2[i]);
f1(1);
f2(2);
}
&mut
only way to encode mutation via reference?(No!)
Cell
+ RefCell
&self
(not &mut self
)Cell<T>
: move values in+out (via swapping, or other methods that ensure cell remains sound, e.g. get/set if T: Copy
)RefCell<T>
provides refs to the T
it holds, but dynamically enforces the rules.RefCell::borrow()
returns read-only Ref
(many-readers), and panic!
's on outstanding mut-borrow (∴ no previous writers)RefCell::borrow_mut()
returns read/write RefMut
(unique), and panic!
's on any outstanding borrow (∴ no previous readers or writers).rayon
)let temp_data = vec!['a', 'b', 'c', '1', '2', '3'];
rayon::scope(|s| {
let (tx, rx) = channel(); // create transmitter/receiver pair
s.spawn(move |_s| { // spawn A1, taking ownership of `rx`
match rx.recv() {
Ok(v) => println!("A1 received {:?}", v),
Err(err) => println!("A1 receive failure {:?}", err),
}
});
let data: &[char] = &temp_data; // (N.B. assigned type is *not* `&Vec`)
let t1 = tx.clone();
// spawn A2, taking ownership of `t1` (and a copy of `data`)
s.spawn(move |_s| t1.send(&data[0..4]).expect("A2 send failure"));
// spawn A3, taking ownership of `tx` (and a copy of `data`)
s.spawn(move |_s| tx.send(&data[2..6]).expect("A3 send failure"));
}); // (`rayon::scope` waits here until all `s.spawn`'ed threads finish)
rx
, t1
, tx
still moved from one thread to another.data
is shared reference to character slice: freely copyables
's are subslices &data[0..4]
+ &data[2..6]
(overlap is safe!)Can send &
-references and &mut
-references
&
-refs copy (as usual).&mut
-refs obey move semantics when sent (as usual)Examples
fn send_ref_i32(arg: &i32) {
rayon::scope(move |s| {
s.spawn(move |_s| println!("arg: {:?}", arg));
});
}
fn send_ref_vec(arg: &Vec<i32>) {
rayon::scope(move |s| {
s.spawn(move |_s| println!("arg: {:?}", arg));
});
}
fn send_mut_vec(arg: &mut Vec<i32>) {
rayon::scope(move |s| {
s.spawn(move |_s| println!("arg: {:?}", arg));
});
}
So far so good
Can send &
-references and &mut
-references ... if data synchronized!
fn send_ref_to_cell(arg: &Cell<i32>) {
rayon::scope(move |s| {
s.spawn(move |_s| println!("arg: {:?}", arg));
});
}
error[E0277]: the trait bound `Cell<i32>: Sync` is not satisfied
--> src/a00.rs:547:5
|
547 | rayon::scope(move |s| {
| ^^^^^^^^^^^^ `Cell<i32>` cannot be shared between threads safely
|
= help: the trait `Sync` is not implemented for `Cell<i32>`
= note: required because of the requirements on the impl of `Send` for `&Cell<i32>`
= note: required because it appears within the type `[closure@src/a00.rs:547:18: 549:6 arg:&Cell<i32>]`
= note: required by `rayon::scope`
"but [...] you get what you need"
Cell<T>
: unsynchronized mutation; incompatible with Sync
.
(Synchronous alternatives include AtomicUsize
, Mutex<T>
)
Send
traitsSend
: focused on ownership transfer
But we already know that move semantics alone does not suffice
(We need our references!)
Send
and Sync
traitsSend
and Sync
control cross-thread capabilitiesT: Send
implies ownership of T
can be tranferred across threadsT: Sync
implies a reference to T
(e.g. &T
, Arc<T>
) can be shared across threadsSend
enables Message Passing style concurrency, via channelsSync
enables Shared State style concurrency; only T
with synchronized access is allowedSend
or Sync
, based on a recursive analysis of their structure.unsafe
stuff, they may need to do so for soundness!)See also: https://www.ralfj.de/blog/2017/06/09/mutexguard-sync.html
Programming in Rust has made me look at C++ code in a whole new light
After experiencing Rust, I dread looking at code from prior projects ... I will now see how riddled with races it was
www.rust-lang.org |
Hack Without Fear |