일반적으로 tokio
를 사용할 때 #[tokio::main]
어트리뷰트를 사용하여 런타임 설정을 자동으로 처리하도록 한다.
그러나 특정 상황에서는 tokio 런타임을 수동으로 관리해야하는 경우가 생긴다. 이에 대해서 알아보도록 하겠다.
기존의 거대한 동기 코드베이스를 한 번에 async
로 바꾸는 것은 거의 불가능에 가깝다. 또한 main
을 async
로 바꾸면 그 main
이 호출하는 다른 함수들도 연쇄적으로 async
가 되어야 하는, 이른바 async
전염성 문제가 발생할 수 있다.
기존 동기 코드에 비동기 코드를 추가하여 고도화할 때, Runtime
객체를 수동으로 생성하는 방식은 거의 필수적인 해결책이며 매우 도움이 된다.
fn main() {
run_heavy_task();
}
fn run_heavy_task() {
let start = Local::now();
println!("start do heavy task");
// 동기
do_heavy_task();
println!("finish heavy task");
let end = Local::now();
let tact_time = end - start;
println!("tact time {} secs", tact_time.num_seconds());
}
fn do_heavy_task() {
std::thread::sleep(Duration::from_secs(2)); // do heavy work
std::thread::sleep(Duration::from_secs(2)); // do heavy work
std::thread::sleep(Duration::from_secs(2)); // do heavy work
// total working time -> 6 secs
}
위 처럼 heavy_task
가 약 6초정도 걸려 시스템이 느리다고 해보자.
동기적인 코드베이스에서 heavy_task
를 빠르게 처리하면 좋을 것 같다.
이런 경우에 비동기 runtime
을 활용하여 비동기적으로 처리해줄 수 있다.
fn main() {
run_heavy_task();
}
fn run_heavy_task() {
let start = Local::now();
println!("start do heavy task");
// 비동기
let rt = Runtime::new().unwrap();
rt.block_on(async {
do_heavy_task_async().await;
});
println!("finish heavy task");
let end = Local::now();
let tact_time = end - start;
println!("tact time {} secs", tact_time.num_seconds());
}
async fn do_heavy_task_async() {
let handle1 = tokio::time::sleep(Duration::from_secs(2));
let handle2 = tokio::time::sleep(Duration::from_secs(2));
let handle3 = tokio::time::sleep(Duration::from_secs(2));
tokio::join!(handle1, handle2, handle3);
}
위 코드처럼 tokio runtime
을 가져와서 block_on
을 통해 현재 스레드를 매개변수로 받은 future
가 종료될 때까지 기다리도록 한다.
2초가 걸리는 task를 동시에 실행을 시켜 속도를 상승시킬 수 있었다.
c++
이나 c#
같은 언어에서는 싱글톤
패턴을 손쉽게 만들 수 있다. 생성자를 private
처리하면 외부에서 객체를 생성할 수 없다.
러스트에서는 어떻게 만들면 좋을까를 고민하다가 아래와 같이 만들어보았다. 정답은 아니다.
pub mod runtime_singleton_mod {
use once_cell::sync::Lazy;
use tokio::runtime::{Handle, Runtime};
static G_RUNTIME: Lazy<RuntimeSingleTon> = Lazy::new(|| {
RuntimeSingleTon(tokio::runtime::Runtime::new().unwrap())
}
);
struct RuntimeSingleTon(Runtime);
pub fn instance() -> Handle {
G_RUNTIME.0.handle().clone()
}
}
runtime_singleton_mod
모듈을 만들어준다. 모듈 내부에서 pub
을 붙이지 않고 RuntimeSingleTon
튜플 구조체를 만들어준다.
G_RUNTIME
이라는 글로벌 변수를 만들어 최초 호출 시 지연 초기화 되도록 했다.
instance
함수를 pub
을 붙여서 만들어서 외부에서는 runtime_singleton_mod
를 통해 instance
메서드만 호출하도록 했다.
호출이 되면 tokio Runtime
의 Handle
을 반환하도록 하였다.
꽤 괜찮은 생각인 것 같다. 그러나 실제로 잘 동작하는지는 테스트해 봐야 할 것 같다.
#[tokio::main]
어트리뷰트를 main
에 붙여서 기본으로 사용만 해왔다. 최근 bevy
의 ecs 프레임워크를 사용하면서 tokio
런타임을 사용했는데 에러가 발생했다. 어떻게 사용할 수 있을까 찾아보니 runtime
을 리소스 형태로 가지고 사용하면 된다고한다.
그리고 위 예제처럼 동기 코드에 비동기 코드를 사용하고 싶을 때 유용하게 사용할 수 있을 것 같다.
더 디테일한 사용방법은 계속 공부할 예정이다.