채널 문서의 검증 코드다.
//! 채널 문서의 주장을 확인한다: 보낸 값의 소유권이 넘어가는 것(E0382), 송신자를 복제한 fan-in,
//! 송신자가 모두 사라져야 수신이 끝나는 것, 수신자가 사라지면 send가 값을 돌려주는 것,
//! sync_channel의 용량 제한과 용량 0(랑데부), Mutex로 감싼 수신자로 하는 fan-out,
//! fan-out에서 `while let`이 잠금을 쥔 채 일해 일꾼들이 한 줄로 서는 것, std::sync::mpmc가 아직 불안정인 것.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/35_channels_verify.rs"
use std::sync::mpsc::{self, RecvTimeoutError, TryRecvError, TrySendError};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
fn compile_errors(code: &str) -> String {
let dir = std::env::temp_dir().join(format!("vr35_{}_{}", 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()
}
/// 생산자 p명이 각자 (번호, 순번)을 n개 보낸다. 한 생산자의 메시지는 보낸 순서대로 온다.
fn fan_in(p: usize, n: usize) -> Vec<(usize, usize)> {
let (tx, rx) = mpsc::channel();
for id in 0..p {
let tx = tx.clone();
thread::spawn(move || {
for i in 0..n {
tx.send((id, i)).unwrap();
}
});
}
drop(tx); // 원본 송신자도 버려야 rx 반복이 끝난다
rx.into_iter().collect()
}
/// 일꾼 w명이 하나의 작업 큐를 나눠 받는다(fan-out). 결과는 다른 채널로 모은다(fan-in).
fn worker_pool(jobs: Vec<u64>, w: usize) -> (u64, Vec<usize>) {
let (job_tx, job_rx) = mpsc::channel::<u64>();
let job_rx = Arc::new(Mutex::new(job_rx)); // Receiver는 하나뿐이라 Mutex로 나눠 쓴다
let (res_tx, res_rx) = mpsc::channel::<(usize, u64)>();
let mut hs = Vec::new();
for id in 0..w {
let job_rx = Arc::clone(&job_rx);
let res_tx = res_tx.clone();
hs.push(thread::spawn(move || {
loop {
// 잠금은 recv 한 번 동안만 쥔다. 가드는 이 문장이 끝나면 풀린다.
let job = job_rx.lock().unwrap().recv();
match job {
Ok(x) => res_tx.send((id, x * x)).unwrap(),
Err(_) => break, // 작업 송신자가 사라졌다
}
}
}));
}
drop(res_tx);
for j in jobs {
job_tx.send(j).unwrap();
}
drop(job_tx);
let mut per_worker = vec![0; w];
let mut sum = 0;
for (id, sq) in res_rx {
per_worker[id] += 1;
sum += sq;
}
for h in hs {
h.join().unwrap();
}
(sum, per_worker)
}
/// 일꾼 4명이 20ms짜리 일 8개를 나눠 한다. 걸린 시간(ms)을 돌려준다.
/// hold_lock = true면 `while let`으로 받아, 일하는 동안에도 수신자 잠금을 쥔다.
fn pool_time(hold_lock: bool) -> u128 {
let (tx, rx) = mpsc::channel::<u32>();
let rx = Arc::new(Mutex::new(rx));
let t = Instant::now();
let hs: Vec<_> = (0..4)
.map(|_| {
let rx = Arc::clone(&rx);
thread::spawn(move || {
if hold_lock {
// 가드가 while let 블록 끝까지 산다: 일하는 동안 다른 일꾼이 recv하지 못한다
while let Ok(_job) = rx.lock().unwrap().recv() {
thread::sleep(Duration::from_millis(20));
}
} else {
loop {
let job = rx.lock().unwrap().recv(); // 가드는 이 문장 끝에서 풀린다
let Ok(_job) = job else { break };
thread::sleep(Duration::from_millis(20));
}
}
})
})
.collect();
for i in 0..8 {
tx.send(i).unwrap();
}
drop(tx);
for h in hs {
h.join().unwrap();
}
t.elapsed().as_millis()
}
fn main() {
// 1. fan-in: 3 생산자 × 1,000개
let got = fan_in(3, 1_000);
assert_eq!(got.len(), 3_000);
for id in 0..3 {
let seq: Vec<usize> = got.iter().filter(|m| m.0 == id).map(|m| m.1).collect();
assert_eq!(seq, (0..1_000).collect::<Vec<_>>());
}
println!("fan-in: 3000 messages, per-producer order kept");
// 2. 송신자가 하나라도 남아 있으면 수신은 끝나지 않는다.
let (tx, rx) = mpsc::channel::<i32>();
let tx2 = tx.clone();
drop(tx);
let first = rx.recv_timeout(Duration::from_millis(50));
assert_eq!(first, Err(RecvTimeoutError::Timeout));
assert_eq!(format!("{first:?}"), "Err(Timeout)");
drop(tx2);
let after = format!("{:?}", rx.recv());
assert_eq!(after, "Err(RecvError)");
println!("recv: Timeout while a clone lives, {after} after all senders dropped");
// 3. 이미 들어간 값은 송신자가 모두 사라진 뒤에도 받을 수 있다.
let (tx, rx) = mpsc::channel();
tx.send("a").unwrap();
tx.send("b").unwrap();
drop(tx);
assert_eq!(rx.recv(), Ok("a"));
assert_eq!(rx.try_recv(), Ok("b"));
assert_eq!(rx.try_recv(), Err(TryRecvError::Disconnected));
println!("buffered messages drain after disconnect, then Disconnected");
// 4. 수신자가 사라지면 send는 Err(SendError(값))로 값을 돌려준다.
let (tx, rx) = mpsc::channel::<String>();
drop(rx);
let back = tx.send(String::from("lost?")).unwrap_err().0;
assert_eq!(back, "lost?");
println!("send after receiver dropped: Err returns the value {back:?}");
// 5. sync_channel(2): 두 개까지는 바로 들어가고 세 번째 try_send는 Full이다.
let (stx, srx) = mpsc::sync_channel::<i32>(2);
stx.try_send(1).unwrap();
stx.try_send(2).unwrap();
assert_eq!(stx.try_send(3), Err(TrySendError::Full(3)));
assert_eq!(srx.recv(), Ok(1));
stx.try_send(3).unwrap();
drop(srx);
assert_eq!(stx.try_send(4), Err(TrySendError::Disconnected(4)));
println!(
"sync_channel(2): third try_send -> Full(3), after one recv -> ok, after receiver dropped -> Disconnected(4)"
);
// 6. sync_channel(0): 받는 쪽이 없으면 try_send도 Full이다(랑데부).
let (ztx, zrx) = mpsc::sync_channel::<i32>(0);
assert_eq!(ztx.try_send(7), Err(TrySendError::Full(7)));
let h = thread::spawn(move || ztx.send(7)); // 받는 쪽이 올 때까지 막힌다
assert_eq!(zrx.recv(), Ok(7));
h.join().unwrap().unwrap();
println!("sync_channel(0): try_send with no receiver waiting -> Full");
// 7. fan-out + fan-in 일꾼 풀
let jobs: Vec<u64> = (1..=100).collect();
let (sum, per_worker) = worker_pool(jobs, 4);
assert_eq!(sum, (1..=100u64).map(|x| x * x).sum::<u64>());
assert_eq!(per_worker.iter().sum::<usize>(), 100);
println!("worker pool: sum of squares {sum}, jobs per worker {per_worker:?}");
// 8. 컴파일 오류: 보낸 값은 더 쓸 수 없고, Receiver는 여러 스레드가 함께 빌릴 수 없다.
let moved = compile_errors(
"pub fn f() { let (tx, _rx) = std::sync::mpsc::channel(); let s = String::from(\"hi\"); tx.send(s).unwrap(); println!(\"{s}\"); }\n",
);
assert!(moved.contains("error[E0382]"));
assert!(moved.contains("borrow of moved value: `s`"));
let share_rx = compile_errors(
"pub fn f() { let (_tx, rx) = std::sync::mpsc::channel::<i32>(); std::thread::scope(|s| { s.spawn(|| rx.try_recv()); s.spawn(|| rx.try_recv()); }); }\n",
);
assert!(share_rx.contains("error[E0277]"));
assert!(share_rx.contains("cannot be shared between threads safely"));
println!("send moves value: E0382; &Receiver across threads: E0277 (Receiver is !Sync)");
// 9. fan-out에서 잠금을 쥐는 범위: let은 나눠 일하고, while let은 한 줄로 선다
let with_let = pool_time(false);
let with_while_let = pool_time(true);
println!("4 workers x 8 jobs of 20ms: let {with_let}ms, while let {with_while_let}ms");
assert!(with_let < 120); // 이론값 2묶음 x 20ms = 40ms
assert!(with_while_let >= 160); // 8개 x 20ms를 한 명씩
// 10. 여러 소비자 채널 std::sync::mpmc는 rustc 1.99에서 아직 불안정 기능이다
let mpmc = compile_errors(
"pub fn f() { let (tx, rx) = std::sync::mpmc::channel::<i32>(); tx.send(1).unwrap(); let _ = rx.recv(); }\n",
);
assert!(mpmc.contains("error[E0658]"));
assert!(mpmc.contains("mpmc_channel"));
println!("std::sync::mpmc: E0658 unstable feature `mpmc_channel`");
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fan_in_counts() {
assert_eq!(fan_in(1, 0).len(), 0);
assert_eq!(fan_in(5, 20).len(), 100);
}
#[test]
fn pool_one_worker() {
let (sum, per) = worker_pool(vec![2, 3], 1);
assert_eq!(sum, 13);
assert_eq!(per, vec![2]);
}
}