Concurrency (Rust)
- 동시성: 프로그램의 서로 다른 일부를 독립적으로 실행하는 것.
- 병렬성: 프로그램의 서로 다른 일부를 같은 시점에 실행하는 것.
Thread
- 많은 프로그래밍 언어가 자체적으로 특별한 스레드 구현을 제공한다:
- 프로그래밍 언어가 제공하는 스레드를 그린 스레드(green thread)라고도 하며, 서로 다른 수의 OS 스레드로 실행된다.
- 그린 스레드 모델을 M:N 모델이라고도 한다:
N개의 OS 스레드 당M개의 그린 스레드가 할당된다.
- 러스트는 저수준 프로그래밍 언어이므로 표준 라이브러리가 1:1 스레딩만 제공한다.
thread::spawn함수를 통해 스레드를 만들 수 있다:use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } }hi number 1 from the main thread! hi number 1 from the spawned thread! hi number 2 from the main thread! hi number 2 from the spawned thread! hi number 3 from the main thread! hi number 3 from the spawned thread! hi number 4 from the main thread! hi number 4 from the spawned thread! hi number 5 from the spawned thread!- 아직 스폰된 스레드의 작업이 끝나지 않았는데도 메인 스레드 작업이 끝나는 바람에 프로그램이 종료되어 버렸다.
thread::spawn함수가 반환하는JoinHandle의join메서드는 스레드가 끝날 때까지 대기하게 만든다:use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }- 스레드 클로저 안에서 메인 스레드의 데이터를 사용하려면 오너십을 이동시켜야 한다:
- 그렇지 않으면 스레드에서 참조하고 있는 데이터가 메인 스레드에서 드롭되는 경우 문제가 생길 수 있다:
use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!("Here's a vector: {:?}", v); }); drop(v); // oh no! handle.join().unwrap(); } - 클로저 앞에
move키워드를 붙이면 클로저가 오너십을 갖게 된다:use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); handle.join().unwrap(); }- 이렇게 하면 클로저가
v에 대한 오너십을 갖고 있으므로 메인 스레드에서v를 드롭할 수 없다. - 러스트의 오너십 규칙이 또 우리를 구했다.
- 이렇게 하면 클로저가
- 그렇지 않으면 스레드에서 참조하고 있는 데이터가 메인 스레드에서 드롭되는 경우 문제가 생길 수 있다:
Async
- 스레드는 단순하지만, 동기화가 어렵고 성능 오버헤드도 크다. 비동기는 오버헤드가 작다.
- 비동기가 스레드보다 항상 더 나은 것은 아니다. 굳이 비동기의 성능이 필요한게 아니면 스레드를 사용하는게 더 쉽다.
- 러스트 표준 라이브러리는 기본적인 동시성 프로그래밍 도구만 제공한다. 따라서 외부 라이브러리를 사용한다:
async는 코드 블록을Future트레잇을 구현하는 상태 머신으로 변환한다:async fn learn_and_sing() { // 노래를 부르기 전에 그 노래를 배울 때까지 기다린다. // 여기서 `block_on` 대신 `.await`을 사용함으로써 스레드 블로킹을 방지하고, // 같은 시점에 `dance`를 실행할 수 있도록 했다. `.await`는 스레드를 블로킹하는 대신 // future가 완료될 때까지 비동기적으로 기다린다. let song = learn_song().await; sing_song(song).await; } async fn async_main() { let f1 = learn_and_sing(); let f2 = dance(); // `join!`은 `.await`와 비슷하지만 여러 futures를 동시에 기다린다. // 만약 `learn_and_sing`가 잠시 블로킹되면, `dance`가 현재 스레드를 점유할 것이다. // 만약 `dance`가 블로킹되면, `learn_and_sing`이 다시 스레드를 점유한다. // 만약 둘 다 블로킹되면 `async_main`이 블로킹되고, 실행자에게 스레드를 양보할 것이다. // `let (book, music) = futures::join!(get_book, get_music);` 처럼 페어를 반환할 수도 있다. futures::join!(f1, f2); } fn main() { // `block_on`은 future가 완료될 때까지 현재 스레드를 블로킹한다. block_on(async_main()); }
Future
Future트레잇은 러스트 비동기 프로그래밍의 핵심.- 대략 이렇게 생겼다:
trait SimpleFuture { type Output; fn poll(&mut self, wake: fn()) -> Poll<Self::Output>; } enum Poll<T> { Ready(T), Pending, }poll: future가 완료됐으면Poll::Ready(result)를, 아니면Poll::Pending을 반환한다.Poll::Pending반환 후 future가 더 진행할 준비가 되면wake함수를 호출한다.wake함수가 호출되면 실행자가 다시poll을 호출함으로써 future를 더 진행시킨다.
자바스크립트와의 차이
- JavaScript는 async-first 언어다.
- 두 가지 면에서 자바스크립트의 promise와 러스트의 futures는 다르다:
- promise는 생성 즉시 스케줄된다. 반면, futures는
await됐을 때만 스케줄된다. - 모든 promies는 fallible하다. 반면, futures는 infallible할 수 있다.
- promise는 생성 즉시 스케줄된다. 반면, futures는
참고자료
- https://rust-lang.github.io/async-book/
- https://doc.rust-lang.org/book/ch16-00-concurrency.html
- https://tokio.rs/tokio/tutorial/hello-tokio
- https://blog.yoshuawuyts.com/futures-concurrency/