원자적 연산과 메모리 순서 문서의 검증 코드다.
//! 원자적 연산과 메모리 순서 문서의 주장을 확인한다: Relaxed 카운터가 정확한 것,
//! 따로 한 load·store는 갱신을 잃는 것, fetch_add가 옛 값을 돌려주고 넘치면 감기는 것,
//! compare_exchange, Release/Acquire 깃발로 데이터 넘기기, Acquire/Release 스핀락,
//! 잘못된 Ordering을 컴파일러가 막는 것.
//! 주의: 이 실행이 통과해도 순서가 옳다는 증명은 아니다. 한 기계(이 Mac은 aarch64)에서 본 결과일 뿐이다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/34_atomics-memory-ordering_verify.rs"
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
use std::thread;
fn compile_errors(code: &str) -> String {
let dir = std::env::temp_dir().join(format!("vr34_{}_{}", std::process::id(), code.len()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("snippet.rs");
std::fs::write(&file, code).unwrap();
let out = std::process::Command::new("rustc")
.args([
"--edition",
"2024",
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir)
.arg(&file)
.output()
.expect("rustc 실행");
String::from_utf8_lossy(&out.stderr).into_owned()
}
/// 스레드 n개가 k번씩 fetch_add(1, Relaxed). 한 변수의 고침 순서는 모두가 같게 보므로 합은 정확하다.
fn relaxed_counter(n: usize, k: usize) -> usize {
let c = AtomicUsize::new(0);
thread::scope(|s| {
for _ in 0..n {
s.spawn(|| {
for _ in 0..k {
c.fetch_add(1, Ordering::Relaxed);
}
});
}
});
c.load(Ordering::Relaxed)
}
/// load와 store를 따로 하면 각 연산은 원자적이어도 "읽고-더하고-쓰기" 전체는 아니다.
/// Barrier로 두 스레드가 모두 읽은 뒤에 쓰게 해서 한쪽 갱신이 사라지는 순서를 매번 만든다.
fn split_load_store() -> usize {
let c = AtomicUsize::new(0);
let barrier = Barrier::new(2);
thread::scope(|s| {
for _ in 0..2 {
s.spawn(|| {
let seen = c.load(Ordering::Relaxed);
barrier.wait();
c.store(seen + 1, Ordering::Relaxed);
});
}
});
c.load(Ordering::Relaxed)
}
/// Release로 깃발을 세우기 전에 쓴 데이터는, Acquire로 그 깃발을 본 쪽에서 반드시 보인다.
fn publish_with_flag(rounds: usize) -> usize {
let mut ok = 0;
for r in 0..rounds {
let data = AtomicU64::new(0);
let ready = AtomicBool::new(false);
let expected = 1_000 + r as u64;
thread::scope(|s| {
s.spawn(|| {
data.store(expected, Ordering::Relaxed);
ready.store(true, Ordering::Release); // 이 앞의 쓰기를 내보낸다
});
s.spawn(|| {
while !ready.load(Ordering::Acquire) {
std::hint::spin_loop();
}
assert_eq!(data.load(Ordering::Relaxed), expected);
});
});
ok += 1;
}
ok
}
/// 가장 작은 스핀락. Mara Bos, Rust Atomics and Locks 4장의 모양을 따른다.
pub struct SpinLock<T> {
locked: AtomicBool,
value: UnsafeCell<T>,
}
// SAFETY: value는 locked를 쥔 스레드 하나만 만진다. 그래서 T: Send이면 여러 스레드가 공유해도 된다.
unsafe impl<T: Send> Sync for SpinLock<T> {}
impl<T> SpinLock<T> {
pub const fn new(value: T) -> Self {
SpinLock {
locked: AtomicBool::new(false),
value: UnsafeCell::new(value),
}
}
/// 잠그고 f를 부른 뒤 푼다. 가드 대신 클로저를 받아 짧게 만들었다.
pub fn with<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
while self.locked.swap(true, Ordering::Acquire) {
std::hint::spin_loop();
}
// SAFETY: swap이 false를 돌려줬으므로 지금 잠금을 쥔 것은 이 스레드뿐이다.
let r = f(unsafe { &mut *self.value.get() });
self.locked.store(false, Ordering::Release);
r
}
}
fn spinlock_counter(n: usize, k: usize) -> u64 {
let lock = Arc::new(SpinLock::new(0u64));
let hs: Vec<_> = (0..n)
.map(|_| {
let lock = Arc::clone(&lock);
thread::spawn(move || {
for _ in 0..k {
lock.with(|v| *v += 1); // 보통 변수(u64)를 고친다
}
})
})
.collect();
for h in hs {
h.join().unwrap();
}
lock.with(|v| *v)
}
/// compare_exchange로 "처음 한 번만" 값을 정한다.
fn first_writer_wins(n: usize) -> (usize, usize) {
let winner = AtomicUsize::new(usize::MAX);
let wins = AtomicUsize::new(0);
thread::scope(|s| {
for id in 0..n {
let (winner, wins) = (&winner, &wins);
s.spawn(move || {
if winner
.compare_exchange(usize::MAX, id, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
wins.fetch_add(1, Ordering::Relaxed);
}
});
}
});
(winner.load(Ordering::Acquire), wins.load(Ordering::Relaxed))
}
fn main() {
// 1. Relaxed 카운터: 4 × 100,000 = 400,000
let c = relaxed_counter(4, 100_000);
assert_eq!(c, 400_000);
println!("Relaxed fetch_add counter: {c}");
// 2. 따로 한 load + store는 갱신을 잃는다.
let lost = split_load_store();
assert_eq!(lost, 1);
println!("separate load/store from 2 threads: {lost} (one update lost)");
// 3. fetch_add는 옛 값을 돌려주고, 넘치면 감긴다(panic하지 않는다).
let a = AtomicU8::new(254);
assert_eq!(a.fetch_add(1, Ordering::Relaxed), 254);
assert_eq!(a.fetch_add(1, Ordering::Relaxed), 255);
assert_eq!(a.load(Ordering::Relaxed), 0);
println!("AtomicU8 254 -> fetch_add returns 254, 255, then value 0 (wraps)");
// 4. compare_exchange: 여러 스레드 중 정확히 하나만 이긴다.
let (w, wins) = first_writer_wins(8);
assert!(w < 8);
assert_eq!(wins, 1);
let x = AtomicUsize::new(5);
assert_eq!(
x.compare_exchange(4, 9, Ordering::Relaxed, Ordering::Relaxed),
Err(5)
);
assert_eq!(
x.compare_exchange(5, 9, Ordering::Relaxed, Ordering::Relaxed),
Ok(5)
);
println!("compare_exchange: exactly one winner of 8 (id {w}); Err(current) on mismatch");
// 5. Release/Acquire 깃발: 2,000번 모두 데이터가 보였다.
let ok = publish_with_flag(2_000);
assert_eq!(ok, 2_000);
println!("Release/Acquire flag: {ok}/2000 rounds saw the data");
// 6. Acquire/Release 스핀락이 보통 u64를 지킨다.
let s = spinlock_counter(4, 50_000);
assert_eq!(s, 200_000);
println!("SpinLock counter: {s}");
// 7. 잘못된 Ordering은 컴파일 오류다(invalid_atomic_ordering, 기본이 deny).
let bad_load = compile_errors(
"use std::sync::atomic::{AtomicBool, Ordering};\npub fn f(a: &AtomicBool) -> bool { a.load(Ordering::Release) }\n",
);
assert!(bad_load.contains("atomic loads cannot have `Release` or `AcqRel` ordering"));
let bad_store = compile_errors(
"use std::sync::atomic::{AtomicBool, Ordering};\npub fn f(a: &AtomicBool) { a.store(true, Ordering::Acquire) }\n",
);
assert!(bad_store.contains("atomic stores cannot have `Acquire` or `AcqRel` ordering"));
println!("load(Release) / store(Acquire): compile error (invalid_atomic_ordering)");
// 8. 원자 타입은 Sync라서 &로 여러 스레드가 함께 쓴다. static에도 둘 수 있다.
static HITS: AtomicUsize = AtomicUsize::new(0);
thread::scope(|s| {
for _ in 0..3 {
s.spawn(|| HITS.fetch_add(1, Ordering::Relaxed));
}
});
assert_eq!(HITS.load(Ordering::Relaxed), 3);
println!("static AtomicUsize hits: 3");
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counters() {
assert_eq!(relaxed_counter(1, 10), 10);
assert_eq!(relaxed_counter(8, 1_000), 8_000);
assert_eq!(spinlock_counter(2, 1_000), 2_000);
}
#[test]
fn split_always_loses_one() {
for _ in 0..20 {
assert_eq!(split_load_store(), 1);
}
}
}