Mutex와 RwLock 문서의 검증 코드다.

//! Mutex와 RwLock 문서의 주장을 확인한다: 가드가 사라질 때 풀리는 것, 독(poisoning)과 복구,
//! RwLock의 읽기 여럿·쓰기 하나, 읽는 쪽 panic은 독을 남기지 않는 것, 잠금 순서, 2024 if let 범위,
//! `let _ = m.lock()`과 `.await` 너머로 가드를 쥐는 코드가 막히는 것(drop(g)로는 안 풀리고 블록으로 감싸야 풀린다).
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/32_mutex-rwlock_verify.rs"

use std::sync::{Arc, Mutex, RwLock, TryLockError};
use std::thread;

fn compile_errors(code: &str) -> String {
    let dir = std::env::temp_dir().join(format!("vr32_{}_{}", 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번씩 1을 더한다.
fn counter(n: usize, k: usize) -> usize {
    let total = Arc::new(Mutex::new(0usize));
    let handles: Vec<_> = (0..n)
        .map(|_| {
            let total = Arc::clone(&total);
            thread::spawn(move || {
                for _ in 0..k {
                    *total.lock().unwrap() += 1;
                }
            })
        })
        .collect();
    for h in handles {
        h.join().unwrap();
    }
    *total.lock().unwrap()
}

struct Account {
    id: u32,
    balance: Mutex<i64>,
}

/// 두 계좌를 잠글 때 항상 id가 작은 쪽부터 잠근다. 방향이 달라도 순서는 같다.
fn transfer(from: &Account, to: &Account, amount: i64) {
    let (first, second) = if from.id < to.id {
        (from, to)
    } else {
        (to, from)
    };
    let mut a = first.balance.lock().unwrap();
    let mut b = second.balance.lock().unwrap();
    if from.id < to.id {
        *a -= amount;
        *b += amount;
    } else {
        *b -= amount;
        *a += amount;
    }
}

fn main() {
    std::panic::set_hook(Box::new(|_| {})); // 일부러 낸 panic의 메시지를 숨긴다

    // 1. 카운터: 4 스레드 × 10,000번 = 40,000
    let c = counter(4, 10_000);
    assert_eq!(c, 40_000);
    println!("Arc<Mutex<usize>> counter: {c}");

    // 2. 가드가 살아 있는 동안 다른 잠금은 실패하고, 가드가 사라지면 풀린다.
    let m = Mutex::new(vec![1, 2]);
    {
        let mut g = m.lock().unwrap();
        g.push(3);
        assert!(matches!(m.try_lock(), Err(TryLockError::WouldBlock)));
    } // g가 여기서 drop되어 풀린다
    assert_eq!(*m.try_lock().unwrap(), vec![1, 2, 3]);
    println!("guard alive: try_lock WouldBlock; after drop: ok");

    // 3. 같은 스레드에서 두 번 잠그기: lock()은 멈추거나 panic할 수 있으므로 try_lock으로만 본다.
    let g = m.lock().unwrap();
    assert!(m.try_lock().is_err());
    drop(g);

    // 4. 독: 잠근 채 panic한 스레드가 있으면 다음 lock()은 Err다. into_inner로 복구할 수 있다.
    let shared = Arc::new(Mutex::new(0));
    let s2 = Arc::clone(&shared);
    let r = thread::spawn(move || {
        let mut g = s2.lock().unwrap();
        *g = 41; // 불변식을 반쯤 고친 상태에서
        panic!("half-updated");
    })
    .join();
    assert!(r.is_err());
    assert!(shared.is_poisoned());
    let mut g = match shared.lock() {
        Ok(g) => g,
        Err(poisoned) => poisoned.into_inner(),
    };
    assert_eq!(*g, 41); // panic 전에 쓴 값이 그대로 남아 있다
    *g += 1;
    drop(g);
    shared.clear_poison();
    assert!(!shared.is_poisoned());
    assert_eq!(*shared.lock().unwrap(), 42);
    println!("poison: lock() Err, into_inner -> 41, clear_poison -> ok");

    // 5. RwLock: 읽기 가드 둘이 동시에 있고, 그동안 쓰기는 막힌다.
    let rw = RwLock::new(String::from("v1"));
    {
        let r1 = rw.read().unwrap();
        let r2 = rw.read().unwrap();
        assert_eq!((r1.as_str(), r2.as_str()), ("v1", "v1"));
        assert!(matches!(rw.try_write(), Err(TryLockError::WouldBlock)));
    }
    rw.write().unwrap().push_str("+w");
    {
        let _w = rw.write().unwrap();
        assert!(matches!(rw.try_read(), Err(TryLockError::WouldBlock)));
    }
    assert_eq!(*rw.read().unwrap(), "v1+w");
    println!("RwLock: two readers at once, writer blocked; writer blocks readers");

    // 6. 읽는 쪽의 panic은 독을 남기지 않고, 쓰는 쪽의 panic은 남긴다.
    let rw = Arc::new(RwLock::new(0));
    let rr = Arc::clone(&rw);
    let _ = thread::spawn(move || {
        let _r = rr.read().unwrap();
        panic!("reader");
    })
    .join();
    assert!(!rw.is_poisoned());
    let rw2 = Arc::clone(&rw);
    let _ = thread::spawn(move || {
        let _w = rw2.write().unwrap();
        panic!("writer");
    })
    .join();
    assert!(rw.is_poisoned());
    println!("RwLock poison: reader panic -> no, writer panic -> yes");

    // 7. 잠금 순서: 반대 방향 이체를 동시에 많이 해도 멈추지 않고 합이 그대로다.
    let a = Arc::new(Account {
        id: 1,
        balance: Mutex::new(1_000),
    });
    let b = Arc::new(Account {
        id: 2,
        balance: Mutex::new(1_000),
    });
    let hs: Vec<_> = (0..4)
        .map(|i| {
            let (a, b) = (Arc::clone(&a), Arc::clone(&b));
            thread::spawn(move || {
                for _ in 0..10_000 {
                    if i % 2 == 0 {
                        transfer(&a, &b, 1);
                    } else {
                        transfer(&b, &a, 1);
                    }
                }
            })
        })
        .collect();
    for h in hs {
        h.join().unwrap();
    }
    let (x, y) = (*a.balance.lock().unwrap(), *b.balance.lock().unwrap());
    assert_eq!((x, y), (1_000, 1_000));
    println!("lock ordering: 40,000 opposite transfers finished, balances {x} {y}");

    // 8. 2024 에디션: if let의 임시 가드는 else에 들어가기 전에 풀린다.
    let opt = Mutex::new(None::<i32>);
    let relocked = if let Some(v) = *opt.lock().unwrap() {
        v
    } else {
        // 2021이었다면 여기서 바깥 가드가 아직 살아 있어 try_lock이 실패한다
        *opt.try_lock().expect("2024: guard already dropped") = Some(7);
        -1
    };
    assert_eq!(relocked, -1);
    assert_eq!(*opt.lock().unwrap(), Some(7));
    println!("edition 2024 if-let: guard dropped before else");

    // 9. 컴파일러가 막는 실수
    let underscore = compile_errors(
        "use std::sync::Mutex;\npub fn f(m: &Mutex<i32>) { let _ = m.lock().unwrap(); }\n",
    );
    assert!(underscore.contains("let_underscore_lock"));
    assert!(underscore.contains("non-binding let on a synchronization lock"));
    println!("let _ = m.lock(): error (let_underscore_lock)");

    let across_await = compile_errors(
        "use std::sync::Mutex;\nasync fn other() {}\nfn need_send<T: Send>(_: T) {}\npub fn f(m: &'static Mutex<i32>) { need_send(async move { let g = m.lock().unwrap(); other().await; drop(g); }); }\n",
    );
    assert!(across_await.contains("error"));
    assert!(across_await.contains("future cannot be sent between threads safely"));
    println!("MutexGuard held across .await: future is not Send");

    let scoped = compile_errors(
        "use std::sync::Mutex;\nasync fn other() {}\nfn need_send<T: Send>(_: T) {}\npub fn f(m: &'static Mutex<i32>) { need_send(async move { { let mut g = m.lock().unwrap(); *g += 1; } other().await; }); }\n",
    );
    assert!(!scoped.contains("error"), "{scoped}");
    let dropped = compile_errors(
        "use std::sync::Mutex;\nasync fn other() {}\nfn need_send<T: Send>(_: T) {}\npub fn f(m: &'static Mutex<i32>) { need_send(async move { let mut g = m.lock().unwrap(); *g += 1; drop(g); other().await; }); }\n",
    );
    assert!(dropped.contains("future cannot be sent between threads safely"));
    println!("guard in inner block before .await: Send; drop(g) before .await: still not Send");

    let _ = std::panic::take_hook();
    println!("ALL CHECKS PASSED");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn counter_small() {
        assert_eq!(counter(1, 0), 0);
        assert_eq!(counter(3, 7), 21);
    }

    #[test]
    fn transfer_both_directions() {
        let a = Account {
            id: 1,
            balance: Mutex::new(10),
        };
        let b = Account {
            id: 2,
            balance: Mutex::new(0),
        };
        transfer(&a, &b, 3);
        transfer(&b, &a, 1);
        assert_eq!(*a.balance.lock().unwrap(), 8);
        assert_eq!(*b.balance.lock().unwrap(), 2);
    }
}