Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- ### Principles of Programming Languages
- ### Assignment 1: Concurrency in Rust
- #### Preliminaries
- Rust is a low-level language, designed with the goals of zero-overhead memory safety, fearless concurrency, and blazing-fast performance.
- Other languages implement concurrency in an opinionated matter, forcing all the code to follow very specific patterns that allow the language to provide good concurrency support, such as clojure's `atom` structure, or Python's Global Interpreter Lock, or Lua's coroutines.
- Rust is a low-level language, however, so the overhead of that level of abstraction is (usually) unacceptable.
- Rust supports basic threading, message passing, and shared memory out-of-the-box.
- Although not originally intended for the purpose, Rust's memory safety mechanisms also make threading much easier and safer.
- Combined with Rust's strong typing mechanism, most threading errors get caught at compile-time, and thus the bold claim "fearless concurrency".
- #### Language builtins
- The Rust language itself provides almost no support for threading.
- The main support it provides are two marker `traits`:
- * `send`: This trait declares that the type can be safely send across threads.
- * `sync`: This trait declares that the type can safely be accessed from multiple threads.
- All the actual threading and sync support is provided by the standard library.
- #### Threading
- Threads are usually created through the `std::thread::spawn` function, which takes a `closure` and runs it in a new `thread`.
- ```rust
- use std::thread;
- fn main() {
- thread::spawn(|| {
- for i in 1..3 {
- println!("{}", i);
- }
- });
- for i in 1..3 {
- println!("hi number {} from the main thread!", i);
- }
- }
- ```
- This produces the familiar race condition, with two threads both trying to print something as fast as possible.
- The `thread::spawn` function returns a `join` handle, which can be used to perform thread joins:
- ```rust
- let handle = thread::spawn(|| {
- for i in 1..10 {
- println!("hi");
- }
- });
- handle.join();
- ```
- The closure is not allowed to access any elements in the surrounding function, since it is possible that it would outlive the scope of the surrounding function(dangling pointers).
- To solve this, the clojure can be declared as a `move` closure, which automatically takes ownership of any variables it needs.
- ```rust
- fn main() {
- let v = 1;
- let handle = thread::spawn(move || {
- println!("{:?}", v);
- });
- handle.join();
- }
- ```
- #### Message Passing
- Message passing channels are the general preferred way to synchronize threads in Rust.
- This is implemented as the `std::sync::mspc` function:
- (`mspc` stands for multiple-producer, single-consumer)
- ```rust
- let (tx, rx) = mpsc::channel();
- ```
- `rx` and `tx` are handles to the receiver and transmitter end of this channel, respectively.
- Usage is something like this:
- ```rust
- fn main() {
- let (tx, rx) = mpsc::channel();
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- });
- let received = rx.recv().unwrap();
- println!("Got: {}", received);
- }
- ```
- An interesting thing to note here is that message passing does not pass the value of the variable.
- It passes ownership of the variable to the other side.
- This means that this channel performs shared memory through message passing, a clever solution that enables the easily understandable channel semantics while avoiding all the overhead from copying values.
- #### Shared memory
- Shared memory is generally frowned upon by the Rust language as it violates Rust's core principle of single ownership.
- However, it is possible to implement safely though the `Mutex` smart pointer.
- `Mutex` is defined in `std::sync::Mutex`
- It is a generic type, and it is used to wrap a variable, granting access to only one thread at a time.
- ```rust
- let m = Mutex::new(5);
- let mut num = m.lock().unwrap();
- *num = 6;
- ```
- A thread can request access to the contents through the `lock` function, which will block until it acquires the lock, and then return the variable(and gain ownership of it).
- In addition, the mutex will automatically unlock when it goes out of scope, solving a class of issues in other languages caused by a thread that forgets to release their lock.
- Unfortunately, mutexes still suffer from deadlock issues.
- In addition, due to Rust's single-owner semantics a single mutex variable cannot be owned by multiple threads, so in practice a mutex is almost always wrapped in a `std::sync::Arc<T>` type, which is a type of reference counter variable that updates atomically, ensuring thread-safety.
Add Comment
Please, Sign In to add comment