rust tokio runtime

wangki·2025년 8월 4일
0

Rust

목록 보기
44/49

tokio runtime

개요

일반적으로 tokio를 사용할 때 #[tokio::main] 어트리뷰트를 사용하여 런타임 설정을 자동으로 처리하도록 한다.
그러나 특정 상황에서는 tokio 런타임을 수동으로 관리해야하는 경우가 생긴다. 이에 대해서 알아보도록 하겠다.

내용

기존의 거대한 동기 코드베이스를 한 번에 async로 바꾸는 것은 거의 불가능에 가깝다. 또한 mainasync로 바꾸면 그 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를 동시에 실행을 시켜 속도를 상승시킬 수 있었다.

runtime을 전역으로 가지고 있으려면?

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 RuntimeHandle을 반환하도록 하였다.

꽤 괜찮은 생각인 것 같다. 그러나 실제로 잘 동작하는지는 테스트해 봐야 할 것 같다.

결론

#[tokio::main] 어트리뷰트를 main에 붙여서 기본으로 사용만 해왔다. 최근 bevy의 ecs 프레임워크를 사용하면서 tokio 런타임을 사용했는데 에러가 발생했다. 어떻게 사용할 수 있을까 찾아보니 runtime을 리소스 형태로 가지고 사용하면 된다고한다.
그리고 위 예제처럼 동기 코드에 비동기 코드를 사용하고 싶을 때 유용하게 사용할 수 있을 것 같다.
더 디테일한 사용방법은 계속 공부할 예정이다.

0개의 댓글