시작하기

러스트의 비동기 프로그래밍에 오신 것을 환영합니다! 비동기 러스트 코드를 작성하기 위해 보는 거라면 제대로 오신 것입니다. 웹 서버든 데이버베이스든 운영체제든, 구축하기 위하여 이 책은 하드웨어의 최대한 사용할 수 있는 러스트의 비동기 프로그래밍 도수를 어떻게 사용하는 지 알려 줄 것입니다.

이 책은 러스트의 비동기 언어 기능과 라이브러리를 사용하기 위한 포괄적이고 최신 가이드를 목표하며 입문자와 전문가를 대상으로 합니다.

  • 초기 장들에서는 포괄적인 비동기 프로그래밍과 러스트에 있는 비동기가 어떻게 사용할 수 있는 지 소개합니다.
  • 중간 장들에서는 비동기 코드를 작성할 때 사용할 수 있는 핵심 유틸리티와 흐름 제어 도구에 대해서 논의하고 퍼포먼스와 재사용성을 최대화한 라이브러리와 애플리케이션을 구축하기 위한 최고의 실습을 서술합니다.
  • 마지막 장들에서는 폭넓은 비동기 생태계를 알려주고 일반적인 작업을 수행하는 방법에 대한 많은 예제를 제공합니다.

왜 비동기인가?

우리 모두 안전한 소프트웨어를 빠르게 작성하게 해주는 러스트를 얼마나 좋아하는가! 그러나 왜 비동기 코드를 작성해야 하는가?

비동기 코드는 동일한 커널 레벨 스레드에서 다수의 테스크를 동시적으로 실행할 수 있게 해줍니다. 전통적인 멀티 스레드 애플리케이션에서는 만약 동시에 다른 두 웹페이지를 다운로드하려면 이렇게 두 스레드에서 일을 작업해야 했습니다.

fn get_two_sites() {
    // Spawn two threads to do work.
    let thread_one = thread::spawn(|| download("https://www.foo.com"));
    let thread_two = thread::spawn(|| download("https://www.bar.com"));

    // Wait for both threads to complete.
    thread_one.join().expect("thread one panicked");
    thread_two.join().expect("thread two panicked");
}

이것은 많은 애플리케이션에서 잘 동작합니다. 결국 스레드들은 한 번에 여러 다른 작업을 동작시키기 위해 디자인되었습니다. 그러나, 이런 방식은 몇몇 제한을 가지고 오기도 하는데 한 프로세스 내에서 스레드들의 스위칭를 하며 스레드 사이에서 데이터를 공유하느라 많은 오버헤드가 생깁니다. 심지어 스레드가 아무런 일도 안 하고 가만히 있더라도 귀한 시스템 리소스를 사용합니다. 이런 비용를 제거하기 위해 비동기 코드가 디자인되었습니다. 따라서 위의 함수를 러스트의 async/.await 표기를 이용하여 재작성할 수 있습니다. async/.await는 복수의 스레드를 생성하지 않아도 동시에 여러 작업을 돌릴 수 있게 해줍니다.

async fn get_two_sites_async() {
    // Create two different "futures" which, when run to completion,
    // will asynchronously download the webpages.
    let future_one = download_async("https://www.foo.com");
    let future_two = download_async("https://www.bar.com");

    // Run both futures to completion at the same time.
    join!(future_one, future_two);
}

종합하여 비동기 애플리케이션은 거의 동일한 작업을 하는 멀티 스레드 구현보다 더 빠르고 더 적은 리소스를 사용할 수 있는 잠재려을 가집니다. 그러나 대가가 존재합니다. 스레드는 운영체제에서 네이티브하게 지원되고 그다지 특별한 프로그래밍 모델, 예를 들면 어떤 함수든 스레드를 생성할 수 있고 스레드를 사용하기 위한 함수를 호출하는 것은 다른 어떤 일반적인 함수들만큼 쉽습니다,이 없습니다. 그러나 비동기 함수는 언어나 라이브러리에서 별도의 지원이 필요합니다. 러스트에서는 async fnFuture를 반환하는 비동기 함수를 만듭니다. 함수 내용을 실행하기 위해서는 반환된 Future이 완료를 위해서 실행되어야 합니다.

전통적인 멀티스레드 애플리케이션은 꽤 효과적이었고 러스트의 작은 메모리 공간과 async를 사용하지 않아도 얻을 수 있는 예측 가능성를 안 잊는 것은 중요합니다. 비동기 프로그래밍 모델의 증가된 복잡함은 언제나 할 가치가 있는 것은 아니고 무슨 애플리케이션을 고려하든 더 단순한 스레드 모델을 사용하는 것이 중요합니다.

비동기 러스트의 상태

비동기 러스트 생태계를 오랫동한 혁신를 경험했습니다. 그래서 어떤 도구를 사용할 것인지, 어떤 라이브러리에 투자할 것인지, 혹은 어떤 문서를 읽을 것인지 고르는 것은 힘듭니다. 그러나 표준 라이브러리의 Future 트레잇과 async/await 언어 기능이 최근에 안정화되었습니다. 따라서 비동기 러스트 생태계는 새로이 안정화된 API로 중심지가 이동하고 있기 때문에 변경 점이 곧 급격하게 줄어들 것입니다.

그러나 최근에 비동기 러스트 생태계는 여전히 급속한 개발 아래에 있고 비동기 러스트 경험은 세련되지 못 합니다. 대부분의 라이브러리들은 여전히 futures크레이트의 0.1 버전을 사용하고 있는데 상호호환을 위해서는 개발자들이 0.3 futures크레이트의 compat 기능에 닿아야 한다는 것을 뜻합니다. async/await 언어 기능은 여전히 새롭습니다. 트레잇 메소드들 안에 있는 async fn구문같은 중요한 확장들은 여전히 안 구현되었고, 현재 컴파일러의 에러 메시지는 해석하기 어려울 수 있습니다.

즉, 러스트는 부족하지만 비동기 프로그래밍을 지원하는 사람에게 적합한 성능 지향의 길이 진척되어 있습니다. 만약에 무엇인가 동굴 탐험이 두렵지 않다면 러스트의 비동기 프로그래밍 세계에 빠지며 즐기세요!

async / .await 입문

async / .await는 러스트의 동기 코드처럼 보이는 비동기 함수를 작성하기 위한 내장 도구입니다. asyncFuture라 하는 트레잇를 구현하는 상태 머신으로 코드블럭을 이동시킵니다. 동기 메소드안에 있는 블로킹 함수를 호출하는 것은 전체 스레드를 블록시키지만 블록된 Future는 스레드 제어를 양보할 것이고 다른 Future가 실행할 수 있게 만듭니다.

비동기 함수를 만들기 위해 async fn 구문을 사용할 수 있습니다.

async fn do_something() { ... }

async fn에서 반환되는 값은 Future입니다. 뭔가가 일어나기 위하여 Future는 실행자(executor)위에서 동작할 필요가 있습니다.

// `block_on`은 현재 스레드가 제공받은 future가 완료될 때까지 블로킹합니다
// 다른 실행자들은 더 복잡한 동작, 예를 들어 하나의 스레드에서 여러 future를 스케줄링하는 것,을 제공합니다.
use futures::executor::block_on;

async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    let future = hello_world(); // Nothing is printed
    block_on(future); // `future` is run and "hello, world!" is printed
}

async fn안에서 Future트레잇를 구현한 다른 타입이 완료되는 것을 기다리기 위해서 .await를 사용할 수 있습니다. 예를 들면 또 다른 async fn의 결과값같은 것을요. block_on과 다르게 await은 현재 스레드를 블로킹하지 않습니다만 대신에 만약에 그 future가 진행이 불가능 하다면 다른 작업이 동작하는 것을 허용하며 future가 완료되는 것을 비동기적으로 기다립니다.

예를 들면 세 async_fnlearn_song, sing_song, dance가 있다고 해 봅시다.

async fn learn_song() -> Song { ... }
async fn sing_song(song: Song) { ... }
async fn dance() { ... }

배우고, 부르고 추는 한 가지 방법은 각각을 개별적으로 브로킹하는 것일 것입니다.

fn main() {
    let song = block_on(learn_song());
    block_on(sing_song(song));
    block_on(dance());
}

허나 우리는 최고의 퍼포먼스를 가능한 이 방법에게 주지 않았는데 오직 한 번에 오직 하나만을 하고 있습니다! 명확하게 노래를 부르기 전에 노래를 배워야 하지만 노래를 배우고 부르는 시간에 같이 댄스를 추는 것이 가능합니다. 이것을 하기 위해서 우리는 동시적으로 실행되는 async fn를 두 개로 나누어 만들 수 있습니다.

async fn learn_and_sing() {
    // Wait until the song has been learned before singing it.
    // We use `.await` here rather than `block_on` to prevent blocking the
    // thread, which makes it possible to `dance` at the same time.
    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` future에서 일시적으로 브로킹 된다면 `dance` future는 현재 스레드를 인수할 것입니다.
    //  만약에 `dance`가 블로킹이 된다면 
    // `learn_and_sing`는 다시 현재 스레드를 인수받을 수 있을 것입니다. 만약 둘 다 블로킹 된다면
    // `async_main`는 블로킹될 것이고 실행자에게 양보할 것입니다.
    futures::join!(f1, f2);
}

fn main() {
    block_on(async_main());
}

이 예제서 노래를 배우는 것은 반드시 노래를 부르는 것 전에 일어나야 하지만 배우고 부르는 것 둘 다 춤을 추는 시간과 동시에 일어날 수 있습니다. 만약 block_on(learn_song())learn_and_sing안의 learn_song().await보다 나중에 사용한다면, 이 스레드는 아마 learn_song이 돌아가는 동안에는 어떤 다른 것들을 할 수 없을 것입니다. learn_song future를 .await붙임으로써 learn_song이 블로킹이 되면 다른 작업이 현재 스레드를 인수할 수 있게 만듭니다. 이것은 동일한 스레드에서 복수의 future들이 동시에 완료시키기 위해 실행할 수 있게 합니다.

이제 async/await의 기본을 배웠습니다, 예제를 해보러 가봅시다.

Http서버에 적용해보기

async/ .await를 에코 서버를 구축하는데 사용해 보죠.

시작하기 위하여 rustup update stable를 돌려서 러스트 안정화 버전을 1.39나 그 이후 버전인지 확인합시다. 한 번 이것을 하고 나서 cargo new async-await-echo를 실행하여 새로운 프로젝트를 만들고 나서 cargo new async-await-echo 폴더를 엽시다.

Cargo.toml 파일에 몇가지 dependency을 추가합니다.

# The latest version of the "futures" library, which has lots of utilities
# for writing async code. Enable the "compat" feature to include the
# functions for using futures 0.3 and async/await with the Hyper library,
# which use futures 0.1.
futures = { version = "0.3", features = ["compat"] }

# Hyper is an asynchronous HTTP library. We'll use it to power our HTTP
# server and to make HTTP requests.
hyper = "0.12.9"