55
edits
Changes
no edit summary
== Memory Ownership and Borrowing ==
Rust has a very peculiar way to deal with memory deallocation, security, and ownership. Understanding how memory management works is essential in order to understand how to use parallel programming in Rust. Consider the following example, where we create a vector, push some elements into it, and then print the elements: <source lang="rust">fn main() { let mut vec = Vec::new(); vec.push(0); vec.push(1); vec.push(2); for i in vec.iter() { println!("{}", i); }}</source> Output: <source>012</source> Notice how variables in Rust are immutable (constant) by default - we need to manually specify that the vector can be changed. Failing to do so will result in a compilation error. In the next example, we abstracted the "push" method to a function called "add_to_vec", which takes a vector and a number, and pushes the number into the vector: <source lang="rust">fn add_to_vec(mut v: Vec<i32>, n: i32) { v.push(n);} fn main() { let mut vec = Vec::new(); vec.push(0); vec.push(1); vec.push(2); add_to_vec(vec, 3); for i in vec.iter() { println!("{}", i); }}</source> In a language like C++, a vector passed like this would be copied to the function and modified, leaving the original vector untouched; in a language like Java, a similar logic would work, since objects are passed by reference by default. Rust, on the other hand, has a different approach: sending the vector to a function like this means transferring the ownership of the object completely to that function. This means that after the vector is sent to a function, it is not available to the main function anymore - once the '''add_to_vec''' function ends, the vector will then be deallocated, and the next *for* loop will fail. If we try compiling this code, the compiler will produce an error: <source>error[textE0382]: use of moved value: `vec` --> src/main.rs:13:12 |11 | add_to_vec(vec, 3); | --- value moved here12 | 13 | for i in vec.iter() { | ^^^ value used here after move | = note: move occurs because `vec` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait</source> To fix this, we will have to transfer the ownership of the vector back to the main function. We do this by returning the vector back and reassigning it to the variable '''vec''': <source lang="rust">fn add_to_vec(mut v: Vec<i32>, n: i32) -> Vec<i32> { v.push(n); v // shorthand for returning. notice the lack of ;} fn main() { let mut vec = Vec::new(); vec.push(0); vec.push(1); vec.push(2); vec = add_to_vec(vec, 3); // receiving the ownership back for i in vec.iter() { println!("{}", i); }}</source> Now the output is the expected: <source>0123</source> An easier way to deal with memory ownership is by lending/borrowing memory instead of completely transferring control over it. This is called memory '''borrowing'''. To borrow a value, we use the '''&''' operator: <source lang="rust">fn add_to_vec(v: &mut Vec<i32>, n: i32) { v.push(n);} fn main() { let mut vec = Vec::new(); vec.push(0); vec.push(1); vec.push(2); add_to_vec(&mut vec, 3); for i in vec.iter() { println!("{}", i); }}</source> This will also produce the correct output. In this case, we don't need to send the ownership of the vector back, because it was never transferred, only borrowed. There is one important detail: '''mutable references (&mut T) are only allowed to exist if no other mutable references to that value are active. In other words: only one mutable reference to a value may exist at a time'''. From what was presented above, some problems can be identified when applying parallelism:* If memory ownership is transferred in function calls, we can not transfer the ownership of one something to several threads at the same time - only one thread can have the ownership* Mutable references to something can only exist one at a time, this means that we can not spawn several threads with mutable references to a value These two factors limit the capability of conventional shared memory. For example, the following code would not compile: <source lang="rust">use std::thread; fn add_to_vec(v: &mut Vec<i32>, n: i32) { v.push(n);} fn main() { let mut vec = Vec::new(); vec.push(0); vec.push(1); vec.push(2); let t1 = thread::spawn(move || { add_to_vec(&mut vec, 4); }); add_to_vec(&mut vec, 3); for i in vec.iter() { println!("{}", i); } t1.join();}</source> <source>error[E0382]: use of moved value: `vec` --> src/main.rs:20:19 |13 | let t1 = thread::spawn(move || { | ------- value moved (into closure) here...20 | add_to_vec(&mut vec, 3); | ^^^ value used here after move |</source> Although this seems unnecessarily strict, it has a great advantage: race conditions are impossible - they are checked at compile time, which means that the systems will be safer and less likely to present concurrency problems. To correctly use parallel programming in Rust, we can make use of tools such as Channels, Locks (Mutex), and Atomic pointers.
== Channels ==
== Locks (Mutex) ==