본문 바로가기

Rust Programming

[Rust] 7. Threads

Chapter 7은 concurrent ticket management system을 위한 단원이다.
 
지금까지 본 것들은 single-threaded였을 때고,
이제는 그것을 바꿀 때다.
 


 
그래서 ticket store을 multithreaded로 바꿀 것이다.
그와 관련된 Rust의 중요한 concurrency features를 배울 것인데,
그것들은 다음과 같다.
 

  • std::thread module을 사용한 Threads.
  • channels를 사용한 Message passing
  • Arc, Mutex, RwLock을 사용한 공유 상태
  • Rust의 concurrency guarantees를 encode한 traits인 Send와 Sync

multithreaded systems의 다양한 patterns와 trade-offs에 대해서도 다룰 것이다.
 


먼저 thread가 무엇인가?
 
thread는 OS의 기저에서 관리되는 execution context다.
각 thread는 각자의 stack과 instruction pointer가 있다.
 
하나의 process는 여러개의 threads를 관리할 수 있다.
이 threads는 같은 memory 영역을 공유한다.
이것은 같은 data에 대해 접근할 수 있다는 것을 의미한다.
 
Threads는 논리적인 구성체다.
결국 물리적인 execution unit인 CPU core에서는 한번에 하나의 instructions set을 실행할 수 있다.
CPU cores보다 더 많은 threads들이 존재할 수 있기 때문에,
OS scheduler는 throughput과 responsiveness를 maximize하기 위해 threads에게
CPU time을 분배하면서, 주어진 시간에 어떤 thread를 run해야하는지 관리한다.
 


Rust의 standard library는 std::thread라는 module을 제공한다.
그것은 threads를 생성하고 관리하게 해준다.
 


새로운 threads를 만들고 그것에 대한 code를 실행하기 위해 std::thread::spawn을 사용할 수 있다.

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        loop {
            thread::sleep(Duration::from_secs(1));
            println!("Hello from a thread!");
        }
    });
    
    loop {
        thread::sleep(Duration::from_secs(2));
        println!("Hello from the main thread!");
    }
}

 
위 program을 execute한다면, main thread와 spawned thread가 concurrently하게 run한다는 것을 확인할 수 있다.
각 thread는 독립적으로 작업을 수행한다.
 


main thread가 끝날 때, 전체 process는 exit될 것이다.
spawned thread는 해당 thread가 끝나거나 main thread가 끝날 때까지 running할 것이다.

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        loop {
            thread::sleep(Duration::from_secs(1));
            println!("Hello from a thread!");
        }
    });

    thread::sleep(Duration::from_secs(5));
}

 
위의 예시에서, "Hello from a thread!" message가 다섯 번 print되는 것을 예상할 수 있다.
main thread는 sleep call이 return될 때, 끝날 것이다.
그리고 spawned thread는 전체 process가 exit했기 때문에, 종료될 것이다.
 


또한 spawned thread가 끝날 때까지 기다리려면,
spawn이 return하는 JoinHandle의 method인 join을 이용하면 된다.

use std::thread;
fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });

    handle.join().unwrap();
}

 
이 예시에서 main thread는 spawned thread가 끝날때까지 기다릴 것이다.
그리고 종료할 것이다.
이것은 두 threads간의 synchronization의 형태다.
이는 program이 종료되기 전에 "Hello from a thread!"가 출력되는 것을 보장한다.
 


01_threads의 exercise는 구현하기가 어려웠다.
그래서 30분정도 고민하다 solution을 확인했는데,
solution상에서는 상당히 간단했다.
 
처음에 반 나누는 method를 찾은 것은 잘한 것이었고,
thread를 spawn하고 join하는 과정에서 잘못 안 사실이 있었다.
spawn의 동작 방식과 move, join이 어떤 역할을 하는지 조금 더 알아봐야겠다.
 


먼저 처음에 반 나누는 method에 대해 설명할 것이다.
 
split_at 함수는 벡터를 특정 인덱스에서 둘로 나눈다.
위 exercise의 경우 벡터의 길이의 절반 v.len() / 2에서 나눴다.
결과는 두 슬라이스(&[type])로 반환된다.
 
해당 슬라이스들은 to_vec()를 통해 vec로 변환할 수 있다.
 


 
이전에 배운 것과 같이 thread::spawn을 통해 새로운 thread를 생성하고,
move keyword를 사용하여 특정 variable의 ownership을 thread로 이동시킨다.
 
즉, 여기서 알 수 있는 것은 move는 ownership을 thread로 가져오기 위한 keyword라는 것이다.
thread 내부에서, vec의 iterator를 into_iter()를 통해 생성하여, sum::<type>()호출한다.
이 thread의 결과는 handle에 저장된다.
 
나머지 반의 과정도 위의 과정을 통해 handle에 저장하고,
각 handle의 join()을 호출하여 thread 종료될 때까지 기다린다.
그리고 각 thread가 계산한 합계 값을 반환받아 더하여 최종 결과로 반환한다.
 


 
이러한 결과를 봐서는 자세하게 이해할 수 없었다.
그래서 JoinHandle<T>에 대해 알아봤다.
 
처음 예시인

use std::thread;
fn main() {
    let handle = thread::spawn(|| {
        println!("Hello from a thread!");
    });

    handle.join().unwrap();
}

 
에서 thread::spawn의 내부에서 반환하는 값은 없다.
그래서 handle은 JoinHandle<()> type을 갖는다.
 
JoinHandle<T>는 새로 생성된 thread가 종료되었을 때,
T type의 값을 반환할 수 있다.
이 값은 join() call을 통해 가져올 수 있다.
 
더 구체적으로 들어가면 JoinHandle<T>는 Result type이다.
반환값으로는

  • Ok(T)
  • Err(Box<dyn Any + Send + 'static>)

두 가지로 반환된다.
 
그래서 unwrap()을 해줘야 제대로된 반환값을 얻을 수 있는 것이다.
 


move keyword를 사용해 새로운 thread를 생성하고,
이전 thread(위에서는 main thread)에서 사용했던 변수를 해당 thread에서 사용한다면,
소유권은 이전된다.
변수가 몇 개이든 상관없고 다시 이전 thread로 돌아갈 때,
해당 변수들은 더이상 사용할 수 없다.
 


 
여러 검색을 통해서 thread의 move, spawn, join에 대해 이해하게 됐다.
여기서부터는 꼬리물기 공부법을 통해,
더욱 더 구체적으로 들어갈 것이다.