이전 exercise에서 vector를 둘로 나누지 않고
borrow하려 했다면, 다음과 같은 compiler error가 뜰 것이다.
error[E0597]: `v` does not live long enough
|
11 | pub fn sum(v: Vec<i32>) -> i32 {
| - binding `v` declared here
...
15 | let right = &v[split_point..];
| ^ borrowed value does not live long enough
16 | let left_handle = spawn(move || left.iter().sum::<i32>());
| --------------------------------
argument requires that `v` is borrowed for `'static`
19 | }
| - `v` dropped here while still borrowed
"argument requires that v is borrowed for 'static"
이게 무슨 뜻일까?
'static lifetime은 Rust에서 특별한 lifetime이다.
해당 value가 program의 전체 기간에 대해 유효하다는 것을 의미한다.
thread::sqawn을 통해 생성된 thread는
그것을 생성한 thread보다 오래 살아남을 수 있다.
예를 들어,
use std::thread;
fn f() {
thread::spawn(|| {
thread::spawn(|| {
loop {
thread::sleep(std::time::Duration::from_secs(1));
println!("Hello from the detached thread!");
}
});
});
}
처음 spawn된 thread는 바로 매초마다 message를 출력하는 child thread를 spawn할 것이다.
첫번째 thread는 finish후 exit할 것이다.
첫번째 thread의 finish와 exit이 일어날 때,
그것의 child thread는 전체 process가 running중일 동안,
계속 running할 것이다.
이 상황을 Rust의 말로, child thread가 그것의 parent를 outlive했다고 한다.
spawned thread는 다음과 같은 것들을 할 수 있다.
- 그것을 spawn한 thread를 outlive하기
- program이 종료될 때까지 run하기
program이 종료되기 전에 drop될지 모르는 어떤 values도 borrow하면 안된다.
이것을 위반한다면 use-after-free bug에 노출될 수 있다.
이것이 std::thread::spawn의 signature에서,
넘겨지는 closure가 'static lifetime을 갖기를 요구하는 이유다.
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static
{
// [..]
}
references만이 아니라, Rust의 모든 values는 lifetime이 있다.
특히, Vec와 String과 같이 data를 own하는 type는 'static constraint를 만족시킨다.
만약 그것을 own한다면, 원하는 만큼 오랫동안 그것을 활용할 수 있다.
심지어 그것을 만들었던 function이 return된 후에도 말이다.
'static은 다음과 같이 말할 수 있다.
- 하나의 owned value를 줘
- program의 전체 기간동안 유효한 reference를 줘
첫번째 접근은 이전 exercise를 해결한 방식이다.
기존 vector의 오른쪽 부분과 왼쪽 부분을 hoild하는 새로운 vector를 할당한 후,
이 벡터들을 생성된 thread로 이동시켰다.
두번째 경우에 대해 얘기해보자.
가장 기본적인 경우는 string literals와 같은 static data에 대한 reference다.
let s: &'static str = "Hello world!";
string literals는 complile-time에 알 수 있기 때문에,
Rust는 executable 안에 read-only data segment라고 불리는 영역에 저장한다.
그 영역을 가리키는 모든 references들은 program이 run하는 동안 유효하다.
그들은 'static contract를 만족한다.
02_static exercise는 static slice를 이용해서 하는 것이었다.
static slice에 대한 처음과 반 위치의 참조(slice형태의 참조)를 split_at을 통해 받아오고,
각 thread에게 두 참조를 사용해 계산을 시킨다.
이 때, static slice에 대한 참조이므로, move와 같은 keyword는 필요없다.
그리고 계산한 값을 join 및 unwrap을 통해 받은 후 더해줘서 return하면
test를 통과한다.

이번엔 직접 풀었다.
solution을 확인 했을 때,
move keyword를 사용했고, into_iter()가 아니라 iter()를 사용한 차이점을 발견했다.
그래서 이에 대해 알아봤다.
내가 쓴 코드와 solution 코드는 소유권 처리 방식에 차이점이 있었다.
(1) move keyword를 사용한 코드(solution 코드)
closure 내부에서 외부 변수를 reference하는 대신,
closure가 그 변수를 소유한다.
즉, closure가 생성될 때, left와 right의 소유권이 closure로 이동한다.
이 방식은 특히 'static lifetime이 아닌 변수를 thread에서 사용할 때, 안전하게 처리할 수 있다.
(2) move keyword를 사용하지 않은 코드(내가 쓴 코드)
move를 사용하지 않았기 때문에 Rust compiler가 left와 right를 reference로 capture한다.
left와 right는 'static lifetime을 가지기 때문에, 프로그램 전반적인 lifetime동안
모든 reference가 유효하다.
따라서 소유권을 move하지 않고 reference만으로도 안전하게 작동한다.
즉, 두 코드 모두 유효하다는 뜻이다.
이것은 'static lifetime이 있는 변수를 thread에서 사용하기 때문이고,
이것을 이해하고 쓴다면, 문제가 없다.
'Rust Programming' 카테고리의 다른 글
| [Rust] 7. Scoped threads, Channels (0) | 2024.10.30 |
|---|---|
| [Rust] 7. Leaking memory (0) | 2024.10.14 |
| [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 |