33
33
- Multithreading
34
34
- Mutual Exclusion
35
35
- Atomics
36
+ - Message Passing
36
37
37
38
38
39
---
@@ -641,8 +642,8 @@ We must eliminate one of the following:
641
642
642
643
Take turns! No "cutting in" mid-update.
643
644
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)
646
647
3. ~~Unsynchronized updates (parties can "cut in" mid-update)~~
647
648
648
649
@@ -764,26 +765,25 @@ why it is impossible to create any data races in safe rust.
764
765
765
766
# Approach 2: Atomics
766
767
768
+ One airtight, _ atomic_ update! Cannot be "temporarily incorrect" mid-update.
767
769
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)~~
772
772
3 . Unsynchronized updates (parties can "cut in" mid-update)
773
773
774
774
775
775
---
776
776
777
777
778
- # Approach 2: Atomics
778
+ # Code to Machine Instructions
779
779
780
- The compiler usually translates the following operation...
780
+ Recall that the compiler will usually translate the following operation...
781
781
782
782
``` c
783
783
x += 1 ;
784
784
```
785
785
786
- ...into the machine instruction equivalent of this:
786
+ ...into the machine instruction equivalent of this code :
787
787
788
788
``` c
789
789
int temp = x;
@@ -795,7 +795,7 @@ x = temp;
795
795
---
796
796
797
797
798
- # Approach 2: Atomics
798
+ # Atomics
799
799
800
800
However, we can use an atomic operation like this:
801
801
@@ -812,55 +812,89 @@ x += 1;
812
812
* ` fetch_and_add ` : performs the operation suggested by the name, and returns the value that was previously in memory
813
813
* Also ` fetch_and_sub ` , ` fetch_and_or ` , ` fetch_and_and ` , ...
814
814
815
+
815
816
---
816
817
817
- # Sneak Peak of CAS Atomic
818
+ # Aside: ` compare_and_swap `
818
819
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
823
825
* 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
+
829
828
830
829
<!-- Speaker note:
830
+ Skip over this slide during lecture and students can look at it if they want.
831
+
831
832
Rule of thumb: conventional wisdom is that locking code is perceived as slower than lockless code
832
833
833
834
This does NOT mean that lock-free solutions are more performant than lock-based solutions.
834
835
835
836
Lock-based solutions are slow due to _contention_ for locks, not _presence_ of locks
836
837
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!
838
839
-->
840
+
841
+
839
842
---
840
843
844
+
841
845
# Atomics
846
+
842
847
These atomic operations are also implemented in the Rust standard library.
848
+
843
849
``` rust
844
- use std :: sync :: atomic :: {AtomicIsize , Ordering };
850
+ use std :: sync :: atomic :: {AtomicI32 , Ordering };
845
851
846
- let x = AtomicIsize :: new (0 );
852
+ let x = AtomicI32 :: new (0 );
847
853
848
854
x . fetch_add (10 , Ordering :: SeqCst );
849
855
x . fetch_sub (2 , Ordering :: SeqCst );
850
856
851
857
println! (" Atomic Output: {}!" , x );
852
858
```
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
+
854
893
855
894
---
856
895
857
- # Atomics
858
896
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**
864
898
865
899
866
900
---
0 commit comments