scoped threads는 spawned thread가 parent를 outlive하는 issue를 회피하기 위한 방법이다.
let v = vec![1, 2, 3];
let midpoint = v.len() / 2;
std::thread::scope(|scope| {
scope.spawn(|| {
let first = &v[..midpoint];
println!("Here's the first half of v: {first:?}");
});
scope.spawn(|| {
let second = &v[midpoint..];
println!("Here's the second half of v: {second:?}");
});
});
println!("Here's v: {v:?}");
std::thread::scope 함수는 새로운 scope를 만든다.
만든 scope instance로 spawn method를 쓸 수 있다.
std::thread::spawn과는 달리, scope를 사용해 spawn된 모든 thread들은
scope가 끝나면 automatically join된다.
이전 example을 thread관점으로 보자면 아래와 같다.
let v = vec![1, 2, 3];
let midpoint = v.len() / 2;
let handle1 = std::thread::spawn(|| {
let first = &v[..midpoint];
println!("Here's the first half of v: {first:?}");
});
let handle2 = std::thread::spawn(|| {
let second = &v[midpoint..];
println!("Here's the second half of v: {second:?}");
});
handle1.join().unwrap();
handle2.join().unwrap();
println!("Here's v: {v:?}");
하지만 위 코드는 compile되지 않을 것이다.
compiler는 &v가 'static lifetime이 아니기 때문에 사용할 수 없다고 할 것이다.
std::thread::scope에서는 이를 안전하게 borrow할 수 있다.
scope를 사용한 예시에서 v는 spawning point이전에 생성된다.
scope는 automatically join되기 때문에, scope가 return된 후, v가 drop된다.
automatically join된다는 것은
scope 안에 spawn된 모든 thread가 scope가 return되기 전에 종료됨이 보장되어 있다는 것이다.
그래서 dangling reference가 생길 risk가 없다.
scoped thread exercise는 vec의 요소들의 계산을
두 thread에서 반반 나눠서 하는 것이었다.
이를 scoped thread를 사용하여 하는 것이었고,
합계를 담을 mutable 변수를 두 개 생성해, scoped thread로 계산했다.
그리고 마지막 두 변수를 더해주면 끝이다.

mutable type은 borrow가 동시에 하나만 가능하다는 것을 기억하자.
ticket management system에서는 client-server architecture를 도입할 것이다.
state와 stored tickets를 관리할,
하나의 long-running server thread를 만들 것이다.
그리고 여러개의 client threads를 만들 것이다.
각 client는 state를 바꾸거나 information을 얻기 위해
commands와 queries를 stateful thread에 보낼 수 있다.
client thread들은 concurrent하게 동작한다.
지금까지 아래와 같이 parent-child communication에 한정되어 설명했다.
- parent context로부터 data를 borrow하거나 consume한 spawn된 thread
- spawn된 thread는 join될 때, parent에 data를 return
client-server 설계에서는 충분하지 않다.
Clients는 생성된 이후로 쭉 server thread와 data를 send하거나 receive할 수 있어야한다.
이를 channels를 통해 해결할 수 있다.
Rust의 standard library는 multi-producer, single-consumer (mpsc) channels를
std::sync::mpsc module을 통해 제공한다.
선호에 따른 bounded, unbounded 두개의 channel이 있다.
unbounded version에 집중하여 볼 것인데, 나중에 각 channel의 장단점에 대해 다룰 것이다.
Channel은 다음과 같이 만든다.
use std::sync::mpsc::channel;
let (sender, receiver) = channel();
sender와 receiver를 얻는다.
channel에 data를 넣고 싶으면 sender에 send를 call하면 된다.
channel에서 data를 받고 싶으면 receiver에 recv를 call하면 된다.
Sender는 clonable하다.
여러개의 senders를 만들 수 있고
그것들은 같은 channel에 모든 data를 넣을 것이다.
대신 Receiver는 clonable하지 않다.
하나의 channel에 하나의 receiver만 있을 수 있다.
mpsc(multi-producer single-consumer)의 뜻 그대로를 의미한다.
Sender와 Receiver는 type parameter T에 대해 generic하다.
그것은 channel에서 주고 받는 messages의 type이다.
u64가 될 수도 있고, struct, enum 등등이 될 수 있다.
send와 recv는 모두 fail할 수 있다.
send는 receiver가 drop됐으면, error를 return한다.
recv는 모든 sender가 drop됐고, channel이 비어있으면 error를 return한다.
즉, send와 recv는 channel이 실질적으로 닫혀있을 때, error를 발생시킨다.
channels의 exercise는 생성된 channel의 server 부분을 짜는 것이었다.
receiver로 받은 command에 따라, server에서 해당 동작을 수행하는 것이다.
server는 command가 계속 남아있을 경우 무한루프를 돌며,
command에 해당하는 명령을 수행한다.

처음에 막막하여, solution을 훔쳐봤다.
코드를 샅샅이 보며, 돌아가는 로직을 이해했고,
while let 문을 사용하여 저렇게 동작하게 할 수도 있구나 생각했다.
'Rust Programming' 카테고리의 다른 글
| [Rust] 7. Leaking memory (0) | 2024.10.14 |
|---|---|
| [Rust] 7. 'static lifetime (0) | 2024.10.13 |
| [Rust] 7. Threads (0) | 2024.10.12 |
| [Rust] 6. HashMap, BTreeMap (0) | 2024.10.07 |
| [Rust] 6. Index trait, IndexMut trait (0) | 2024.10.06 |