454: Compare-and-Exchange (CAS)
Tutorial
The Problem
Atomic increment (fetch_add) handles the simple case, but complex atomic updates β "set to max", "set if value is X", "conditional swap" β require compare_and_exchange (CAS). CAS is the universal primitive for lock-free algorithms: read the current value, compute the new value, atomically swap oldβnew only if the value is still what you read. If another thread changed it, retry. This retry loop is the foundation of all lock-free data structures: lock-free stacks, queues, linked lists, and hash maps.
CAS appears in Arc's reference count decrement (is-zero check), lock-free queue implementations, optimistic concurrency control, and AtomicPtr pointer swaps.
🎯 Learning Outcomes
compare_exchange (strong, no spurious failure) and compare_exchange_weak (weak, may spuriously fail β preferred in loops)atomic_max uses CAS to atomically update a maximum value(success_ordering, failure_ordering) parametersCode Example
#![allow(clippy::all)]
// 454. CAS: compare_exchange and loops
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn cas_increment(a: &AtomicUsize) {
let mut cur = a.load(Ordering::Relaxed);
loop {
match a.compare_exchange_weak(cur, cur + 1, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => break,
Err(actual) => cur = actual,
}
}
}
fn atomic_max(a: &AtomicI64, v: i64) {
let mut cur = a.load(Ordering::Relaxed);
loop {
if v <= cur {
break;
}
match a.compare_exchange_weak(cur, v, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => break,
Err(actual) => cur = actual,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cas_inc() {
let a = AtomicUsize::new(0);
for _ in 0..100 {
cas_increment(&a);
}
assert_eq!(a.load(Ordering::SeqCst), 100);
}
#[test]
fn test_cas_fail() {
let a = AtomicUsize::new(5);
assert_eq!(
a.compare_exchange(99, 0, Ordering::SeqCst, Ordering::SeqCst),
Err(5)
);
}
#[test]
fn test_max() {
let m = AtomicI64::new(i64::MIN);
for v in [5, 3, 8, 1, 9, 2] {
atomic_max(&m, v);
}
assert_eq!(m.load(Ordering::SeqCst), 9);
}
}Key Differences
compare_exchange returns Result<old_val, actual_val>; OCaml returns bool, requiring a separate Atomic.get to get the actual value on failure.compare_exchange_weak for loop use; OCaml's single compare_and_set corresponds roughly to compare_exchange (strong).u128 packing tag + pointer) or hazard pointers.OCaml Approach
OCaml 5.x's Atomic.compare_and_set old_val new_val at is the CAS primitive. It returns a bool rather than Result<_, actual>, so you reload the value after failure. OCaml's CAS uses sequential consistency. A CAS-based counter: let cas_inc a = while not (let cur = Atomic.get a in Atomic.compare_and_set a cur (cur+1)) do () done.
Full Source
#![allow(clippy::all)]
// 454. CAS: compare_exchange and loops
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
fn cas_increment(a: &AtomicUsize) {
let mut cur = a.load(Ordering::Relaxed);
loop {
match a.compare_exchange_weak(cur, cur + 1, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => break,
Err(actual) => cur = actual,
}
}
}
fn atomic_max(a: &AtomicI64, v: i64) {
let mut cur = a.load(Ordering::Relaxed);
loop {
if v <= cur {
break;
}
match a.compare_exchange_weak(cur, v, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => break,
Err(actual) => cur = actual,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cas_inc() {
let a = AtomicUsize::new(0);
for _ in 0..100 {
cas_increment(&a);
}
assert_eq!(a.load(Ordering::SeqCst), 100);
}
#[test]
fn test_cas_fail() {
let a = AtomicUsize::new(5);
assert_eq!(
a.compare_exchange(99, 0, Ordering::SeqCst, Ordering::SeqCst),
Err(5)
);
}
#[test]
fn test_max() {
let m = AtomicI64::new(i64::MIN);
for v in [5, 3, 8, 1, 9, 2] {
atomic_max(&m, v);
}
assert_eq!(m.load(Ordering::SeqCst), 9);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cas_inc() {
let a = AtomicUsize::new(0);
for _ in 0..100 {
cas_increment(&a);
}
assert_eq!(a.load(Ordering::SeqCst), 100);
}
#[test]
fn test_cas_fail() {
let a = AtomicUsize::new(5);
assert_eq!(
a.compare_exchange(99, 0, Ordering::SeqCst, Ordering::SeqCst),
Err(5)
);
}
#[test]
fn test_max() {
let m = AtomicI64::new(i64::MIN);
for v in [5, 3, 8, 1, 9, 2] {
atomic_max(&m, v);
}
assert_eq!(m.load(Ordering::SeqCst), 9);
}
}
Exercises
AtomicPtr<Node<T>>. push allocates a node, sets its next to the current head, then CAS-swaps the head from old_head to new_node. Repeat on failure.AtomicU128 or a crate), implement an (version, value) CAS that increments the version on every successful update, preventing the ABA problem.Mutex<u64>. Benchmark both with 8 threads each doing 1M increments. Plot throughput vs. number of threads from 1 to 16.