동시성 연습 문서의 문제 1 풀이 코드다.

//! 동시성 연습 p1: 컴파일되지 않는 병렬 글자 세기를 고친 답.
//! 처음 코드(Rc<RefCell<..>> + thread::spawn)가 E0277로 막히는 것, Arc<Mutex<..>>로만 바꾸면 E0521이 남는 것,
//! 고친 두 답이 같은 결과를 내는 것을 확인한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/4.연습문제/35_concurrency-practice_p1.rs"

use std::sync::Mutex;
use std::thread;

fn compile_errors(code: &str) -> String {
    let dir = std::env::temp_dir().join(format!("vrp1_{}_{}", 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()
}

const BROKEN: &str = r#"
use std::cell::RefCell;
use std::rc::Rc;
use std::thread;

pub fn letter_counts(texts: &[&str]) -> [usize; 26] {
    let counts = Rc::new(RefCell::new([0usize; 26]));
    let mut handles = Vec::new();
    for t in texts {
        let counts = Rc::clone(&counts);
        handles.push(thread::spawn(move || {
            for b in t.bytes().filter(u8::is_ascii_lowercase) {
                counts.borrow_mut()[(b - b'a') as usize] += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    *counts.borrow()
}
"#;

/// Rc<RefCell>만 Arc<Mutex>로 바꾼 판: Send 문제는 풀리지만 빌린 texts가 'static이 아니다.
const HALF_FIXED: &str = r#"
use std::sync::{Arc, Mutex};
use std::thread;

pub fn letter_counts(texts: &[&str]) -> [usize; 26] {
    let counts = Arc::new(Mutex::new([0usize; 26]));
    let mut handles = Vec::new();
    for t in texts {
        let counts = Arc::clone(&counts);
        handles.push(thread::spawn(move || {
            for b in t.bytes().filter(u8::is_ascii_lowercase) {
                counts.lock().unwrap()[(b - b'a') as usize] += 1;
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
    *counts.lock().unwrap()
}
"#;

fn count_one(t: &str) -> [usize; 26] {
    let mut c = [0usize; 26];
    for b in t.bytes().filter(u8::is_ascii_lowercase) {
        c[(b - b'a') as usize] += 1;
    }
    c
}

/// 답 A: 스레드마다 자기 배열을 세어 돌려주고, join한 쪽이 더한다. 잠금이 없다.
fn letter_counts_join(texts: &[&str]) -> [usize; 26] {
    thread::scope(|s| {
        let hs: Vec<_> = texts.iter().map(|t| s.spawn(|| count_one(t))).collect();
        let mut total = [0usize; 26];
        for h in hs {
            for (sum, x) in total.iter_mut().zip(h.join().unwrap()) {
                *sum += x;
            }
        }
        total
    })
}

/// 답 B: 공유 배열을 Mutex로 감싼다. 각 스레드는 다 센 뒤 한 번만 잠근다.
fn letter_counts_mutex(texts: &[&str]) -> [usize; 26] {
    let total = Mutex::new([0usize; 26]);
    thread::scope(|s| {
        for t in texts {
            let total = &total;
            s.spawn(move || {
                let local = count_one(t);
                let mut g = total.lock().unwrap();
                for (sum, x) in g.iter_mut().zip(local) {
                    *sum += x;
                }
            });
        }
    });
    total.into_inner().unwrap()
}

fn main() {
    let errs = compile_errors(BROKEN);
    assert!(errs.contains("error[E0277]"));
    assert!(errs.contains("`Rc<RefCell<[usize; 26]>>` cannot be sent between threads safely"));
    println!("broken: E0277 Rc<RefCell<..>> cannot be sent between threads safely");
    let half = compile_errors(HALF_FIXED);
    assert!(half.contains("error[E0521]"));
    assert!(half.contains("borrowed data escapes outside of function"));
    assert!(!half.contains("E0277"));
    println!("Arc<Mutex<..>> + thread::spawn: E0521 borrowed data escapes (texts is not 'static)");

    let texts = ["hello world", "rust threads", "abc xyz", ""];
    let a = letter_counts_join(&texts);
    let b = letter_counts_mutex(&texts);
    let seq = texts.iter().fold([0usize; 26], |mut acc, t| {
        for (s, x) in acc.iter_mut().zip(count_one(t)) {
            *s += x;
        }
        acc
    });
    assert_eq!(a, seq);
    assert_eq!(b, seq);
    let l = a[(b'l' - b'a') as usize];
    let total: usize = a.iter().sum();
    assert_eq!((l, total), (3, 27));
    println!("answer A == answer B == sequential; 'l' = {l}, letters = {total}");
    println!("ALL CHECKS PASSED");
}

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

    #[test]
    fn empty_input() {
        assert_eq!(letter_counts_join(&[]), [0; 26]);
        assert_eq!(letter_counts_mutex(&[]), [0; 26]);
    }
}