|
| 1 | ++++ |
| 2 | +title = "Fearless concurrency" |
| 3 | +date = 2022-11-28 |
| 4 | +weight = 1 |
| 5 | +[extra] |
| 6 | +lesson_date = 2022-12-12 |
| 7 | ++++ |
| 8 | + |
| 9 | +## Parallelism vs Concurrency |
| 10 | + |
| 11 | +Concurrency is when tasks **can make** progress **independently** of each other. |
| 12 | + |
| 13 | +Parallelism is when multiple tasks **make** progress **at the same time**. |
| 14 | + |
| 15 | +## Concurrency models in Rust |
| 16 | + |
| 17 | +### Threads |
| 18 | + |
| 19 | +Nothing unusual here. |
| 20 | + |
| 21 | +Threads can be created with the `thread::spawn` function [docs - please read them!](https://doc.rust-lang.org/std/thread/fn.spawn.html). |
| 22 | + |
| 23 | +This method returns a `JoinHandle<T>` which can be used to wait for the thread to finish. `T` is the type of the thread's return value. |
| 24 | + |
| 25 | +#### Propagating panics |
| 26 | + |
| 27 | +In Rust a panic of one thread doesn't affect the other threads (similar to how Java handles exceptions in threads). |
| 28 | + |
| 29 | +#### Closures |
| 30 | + |
| 31 | +Closures which are used to create threads must take ownership of any values they use. It can be forced with the `move` keyword. |
| 32 | + |
| 33 | +```rust |
| 34 | +use std::thread; |
| 35 | + |
| 36 | +fn main() { |
| 37 | + let v = vec![1, 2, 3]; |
| 38 | + |
| 39 | + let handle = thread::spawn(move || { |
| 40 | + println!("Here's a vector: {:?}", v); |
| 41 | + }); |
| 42 | + |
| 43 | + handle.join().unwrap(); |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +Normal ownership rules still apply. It means that we cannot mutate the vector in the spawned thread from the main thread! |
| 48 | + |
| 49 | +But what if we need to share some state? |
| 50 | + |
| 51 | +### Message passing |
| 52 | + |
| 53 | +One possible way is to use message passing. We can use a blocking queue (called `mpsc` - ["multi producer single consumer FIFO queue"](https://doc.rust-lang.org/std/sync/mpsc/index.html)) to do it. |
| 54 | +We talked about blocking queues in the Concurrent programming class. In Rust, they are strongly-typed. Sending and receiving ends have different types. |
| 55 | + |
| 56 | +### Mutexes |
| 57 | + |
| 58 | +In Rust, a mutex _wraps_ a value and makes it thread-safe. |
| 59 | +Because it becomes a part of the type, it's impossible to access the underlying value in an unsynchronized manner. It is conceptually similar to the `RefCell` type. |
| 60 | + |
| 61 | +`Arc` is a smart pointer like `Rc` but it can be shared between threads. |
| 62 | + |
| 63 | +Please read more about them in [the book](https://doc.rust-lang.org/stable/book/ch16-03-shared-state.html). |
| 64 | + |
| 65 | +[The docs](https://doc.rust-lang.org/std/sync/struct.Mutex.html) also mention `poisoning`. |
| 66 | + |
| 67 | +### RwLocks |
| 68 | + |
| 69 | +[RwLocks](https://doc.rust-lang.org/std/sync/struct.RwLock.html) are similar to mutexes, but they distinguish between read and write locks. |
| 70 | + |
| 71 | +## Send and Sync |
| 72 | + |
| 73 | +They are marker traits used to indicate that a type or a reference to it can be sent across threads. See the [nomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) for more information. |
| 74 | + |
| 75 | +## Atomic types |
| 76 | + |
| 77 | +Atomic types are described in [the docs](https://doc.rust-lang.org/std/sync/atomic/). |
| 78 | + |
| 79 | +```rust |
| 80 | +use std::sync::Arc; |
| 81 | +use std::sync::atomic::{AtomicUsize, Ordering}; |
| 82 | +use std::{hint, thread}; |
| 83 | + |
| 84 | +fn main() { |
| 85 | + let spinlock = Arc::new(AtomicUsize::new(1)); |
| 86 | + |
| 87 | + let spinlock_clone = Arc::clone(&spinlock); |
| 88 | + let thread = thread::spawn(move|| { |
| 89 | + spinlock_clone.store(0, Ordering::SeqCst); |
| 90 | + }); |
| 91 | + |
| 92 | + // Wait for the other thread to release the lock |
| 93 | + while spinlock.load(Ordering::SeqCst) != 0 { |
| 94 | + hint::spin_loop(); |
| 95 | + } |
| 96 | + |
| 97 | + if let Err(panic) = thread.join() { |
| 98 | + println!("Thread had an error: {:?}", panic); |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +Note that `atomic` values don't have to be wrapped in a mutex when shared across threads. |
| 104 | + |
| 105 | +### Wait... |
| 106 | + |
| 107 | +If most types are `Sync + Send`, then what stops us from using a standard, non-atomic integer in the example above? |
| 108 | + |
| 109 | +```rust |
| 110 | +let spinlock = Arc::new(1); |
| 111 | + |
| 112 | +let spinlock_clone = Arc::clone(&spinlock); |
| 113 | +let thread = thread::spawn(move|| { |
| 114 | + *spinlock_clone += 1; |
| 115 | +}); |
| 116 | + |
| 117 | +while *spinlock != 0 { |
| 118 | + hint::spin_loop(); |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +``` |
| 123 | +error[E0594]: cannot assign to data in an `Arc` |
| 124 | + --> src/main.rs:9:9 |
| 125 | + | |
| 126 | +9 | *spinlock_clone += 1; |
| 127 | + | ^^^^^^^^^^^^^^^^^^^^ cannot assign |
| 128 | + | |
| 129 | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<i32>` |
| 130 | +``` |
| 131 | + |
| 132 | +...so we would have to use a `RefCell` to be able to modify the value through a shared reference... |
| 133 | + |
| 134 | +```rust |
| 135 | +let spinlock = Arc::new(RefCell::new(1)); |
| 136 | + |
| 137 | +let spinlock_clone = Arc::clone(&spinlock); |
| 138 | +let thread = thread::spawn(move|| { |
| 139 | + *spinlock_clone.borrow_mut() += 1; |
| 140 | +}); |
| 141 | + |
| 142 | +// Wait for the other thread to release the lock |
| 143 | +while *spinlock.borrow() != 0 { |
| 144 | + hint::spin_loop(); |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +...but `RefCell` isn't `Sync`: |
| 149 | + |
| 150 | +``` |
| 151 | +error[E0277]: `RefCell<i32>` cannot be shared between threads safely |
| 152 | + --> src/main.rs:9:18 |
| 153 | + | |
| 154 | +9 | let thread = thread::spawn(move|| { |
| 155 | + | ^^^^^^^^^^^^^ `RefCell<i32>` cannot be shared between threads safely |
| 156 | + | |
| 157 | + = help: the trait `Sync` is not implemented for `RefCell<i32>` |
| 158 | + = note: required because of the requirements on the impl of `Send` for `Arc<RefCell<i32>>` |
| 159 | + = note: required because it appears within the type `[closure@src/main.rs:9:32: 11:6]` |
| 160 | +note: required by a bound in `spawn` |
| 161 | +``` |
| 162 | + |
| 163 | +And that bound mentioned in the last line looks like this: |
| 164 | + |
| 165 | +```rust |
| 166 | +pub fn spawn<F, T>(f: F) -> JoinHandle<T> where |
| 167 | + F: FnOnce() -> T, |
| 168 | + F: Send + 'static, |
| 169 | + T: Send + 'static, |
| 170 | +``` |
| 171 | + |
| 172 | +#### Exercise for the reader |
| 173 | + |
| 174 | +Why is it impossible to share a reference to a `Mutex` between threads? |
| 175 | + |
| 176 | +## Data parallelism with Rayon |
| 177 | + |
| 178 | +[Rayon](https://docs.rs/rayon/latest/rayon/) is a library for parallelization of data processing. |
| 179 | +It can be used to parallelize the execution of functions over a collection of data by switching the standard `Iterator` to a `ParallelIterator`. |
| 180 | +It works very similar to [Java's parallel streams](https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html#executing_streams_in_parallel). |
| 181 | + |
| 182 | +Why do that? Because thread synchronization is hard! [Rust prevents data races](https://doc.rust-lang.org/nomicon/races.html), but [logical races and deadlocks are impossible to prevent!](https://users.rust-lang.org/t/deadlock-is-it-a-bug-or-is-it-intentional/1544)! |
| 183 | + |
| 184 | +[Rayon's FAQ](https://github.com/rayon-rs/rayon/blob/master/FAQ.md) is worth reading. |
| 185 | + |
| 186 | +## Reading |
| 187 | + |
| 188 | +- [The Book](https://doc.rust-lang.org/book/ch16-00-concurrency.html) |
| 189 | +- [Safely writing code that isn't thread-safe](http://archive.today/WFlZV) |
0 commit comments