Skip to content

Commit d5299de

Browse files
committed
refactor atomics section
1 parent 0894666 commit d5299de

File tree

1 file changed

+64
-30
lines changed

1 file changed

+64
-30
lines changed

week12/parallelism.md

+64-30
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ code {
3333
- Multithreading
3434
- Mutual Exclusion
3535
- Atomics
36+
- Message Passing
3637

3738

3839
---
@@ -641,8 +642,8 @@ We must eliminate one of the following:
641642
642643
Take turns! No "cutting in" mid-update.
643644
644-
1. `x` is shared memory
645-
2. `x` becomes incorrect mid-update
645+
1. `x` is in shared memory
646+
2. `x` temporarily becomes incorrect (mid-update)
646647
3. ~~Unsynchronized updates (parties can "cut in" mid-update)~~
647648
648649
@@ -764,26 +765,25 @@ why it is impossible to create any data races in safe rust.
764765

765766
# Approach 2: Atomics
766767

768+
One airtight, _atomic_ update! Cannot be "temporarily incorrect" mid-update.
767769

768-
One airtight update! Cannot be "incorrect" mid-update.
769-
770-
1. `x` is shared memory
771-
2. ~~`x` becomes incorrect mid-update~~
770+
1. `x` is in shared memory
771+
2. ~~`x` temporarily becomes incorrect (mid-update)~~
772772
3. Unsynchronized updates (parties can "cut in" mid-update)
773773

774774

775775
---
776776

777777

778-
# Approach 2: Atomics
778+
# Code to Machine Instructions
779779

780-
The compiler usually translates the following operation...
780+
Recall that the compiler will usually translate the following operation...
781781

782782
```c
783783
x += 1;
784784
```
785785

786-
...into the machine instruction equivalent of this:
786+
...into the machine instruction equivalent of this code:
787787

788788
```c
789789
int temp = x;
@@ -795,7 +795,7 @@ x = temp;
795795
---
796796

797797

798-
# Approach 2: Atomics
798+
# Atomics
799799

800800
However, we can use an atomic operation like this:
801801

@@ -812,55 +812,89 @@ x += 1;
812812
* `fetch_and_add`: performs the operation suggested by the name, and returns the value that was previously in memory
813813
* Also `fetch_and_sub`, `fetch_and_or`, `fetch_and_and`, ...
814814

815+
815816
---
816817

817-
# Sneak Peak of CAS Atomic
818+
# Aside: `compare_and_swap`
818819

819-
Other common atomic is `compare_and_swap`
820-
* If the current value matches some old value, then write new value into memory
821-
* Depending on variant, returns a boolean for whether new value was written into memory
822-
* "Lock-free" programming:
820+
Another common atomic operation is `compare_and_swap`
821+
822+
* If the current value matches an old value, update the value
823+
* Returns whether or not the value was updated
824+
* You can do "lock-free" programming with just CAS
823825
* No locks! Just `compare_and_swap` until we successfully write new value
824-
* Not necessarily more performant than lock-based solutions
825-
* Contention is bottleneck, not presence of locks
826-
827-
<!--Speaker note: can skip over this slide during lecture and students can look at it if they want.
828-
-->
826+
* _Not necessarily more performant than lock-based solutions_
827+
829828

830829
<!-- Speaker note:
830+
Skip over this slide during lecture and students can look at it if they want.
831+
831832
Rule of thumb: conventional wisdom is that locking code is perceived as slower than lockless code
832833
833834
This does NOT mean that lock-free solutions are more performant than lock-based solutions.
834835
835836
Lock-based solutions are slow due to _contention_ for locks, not _presence_ of locks
836837
If multiple threads are contending for same memory location, i.e. stuck in a `compare_and_swap` loop, that can be equally slow
837-
This is why benchmarking is importnat, because we can't crystal-ball the performance of our solutions!
838+
This is why benchmarking is important, because we can't crystal-ball the performance of our solutions!
838839
-->
840+
841+
839842
---
840843

844+
841845
# Atomics
846+
842847
These atomic operations are also implemented in the Rust standard library.
848+
843849
```rust
844-
use std::sync::atomic::{AtomicIsize, Ordering};
850+
use std::sync::atomic::{AtomicI32, Ordering};
845851

846-
let x = AtomicIsize::new(0);
852+
let x = AtomicI32::new(0);
847853

848854
x.fetch_add(10, Ordering::SeqCst);
849855
x.fetch_sub(2, Ordering::SeqCst);
850856

851857
println!("Atomic Output: {}!", x);
852858
```
853-
<!-- A trivial example, but gives a brief look at the atomic API in Rust. -->
859+
860+
* The API is largely identical to C++20 atomics
861+
* If interested in the `Ordering`, research "memory ordering"
862+
863+
<!--
864+
A trivial example, but gives a brief look at the atomic API in Rust.
865+
866+
Memory ordering has to do with memory ordering at the CPU cache level.
867+
-->
868+
869+
870+
---
871+
872+
873+
# Atomic Action
874+
875+
Here is an example of incrementing an atomic counter from multiple threads!
876+
877+
```rust
878+
static counter: AtomicUsize = AtomicUsize::new(0);
879+
880+
fn main() {
881+
// Spawn 100 threads that each increment the counter by 1.
882+
let handles: Vec<JoinHandle<_>> = (0..100)
883+
.map(|_| thread::spawn(|| counter.fetch_add(1, Ordering::Relaxed)))
884+
.collect();
885+
886+
// Wait for all threads to finish.
887+
handles.into_iter().for_each(|handle| { handle.join().unwrap(); });
888+
889+
assert_eq!(counter.load(Ordering::Relaxed), 100);
890+
}
891+
```
892+
854893

855894
---
856895

857-
# Atomics
858896

859-
Rust provides atomic primitive types, like `AtomicBool`, `AtomicI8`, `AtomicIsize`, etc.
860-
* Provides a way to access values atomically from any thread
861-
* Safe to share between threads implementing `Sync`
862-
* We won't cover it further in this course, but the API is largely 1:1 with the C++20 atomics
863-
* If interested in pitfalls, read up on *memory ordering* in computer systems
897+
# **Message Passing**
864898

865899

866900
---

0 commit comments

Comments
 (0)