rust Future trait

wangki·2025년 3월 4일

Rust

목록 보기
26/56

개요

asynchronous programming에 대해서 공부한 기록을 남긴다.

future trait 구현 방법에 대해서 알아보겠다.
중요한 poll, waker의 사용방법 및 원리에 대해서 정리할 것이다.

  1. poll 구현
  2. waker 등록 및 사용 방법
  3. 간단한 타이머 예제

내용

poll

futureawait키워드를 사용하게 되면 poll 메서드가 호출이 된다.
작업이 완료되었다면 Poll::Ready를 반환하게 되고 아직 완료되지 않았다면 Poll::Pending이 반환된다.

waker

Poll::Pending이 반환된 이 후 waker를 통해 poll이 다시 호출된다.
보통 waker의 경우 외부 이벤트 완료 시 호출될 수 있도록 등록한다.

use std::{future::Future, task::Poll, time::{Duration, Instant}};

#[tokio::main]
async fn main() {
    let now = Instant::now();
    let duration = Duration::from_secs(2);

    let later = now + duration;
    let timer = Timer::new(
        later,
    );

    timer.await;

    println!("프로그램을 종료합니다.");

}

struct Timer {
    duration: Instant,
} 

impl Timer {
    fn new(duration: Instant) -> Self {
        Timer { 
            duration  
        }
    }
}

impl Future for Timer {
    type Output = ();

    fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
        let now = Instant::now();
        let waker = cx.waker().clone();

        if self.duration < now {
            Poll::Ready(())
        } else {
            // 1. duration을 주기적으로 돌아준다...
            let duration = self.duration;
            tokio::spawn(async move {
                println!("left time: {:?}", duration - now);
                tokio::time::sleep(Duration::from_millis(500)).await;
                waker.wake();
            });

            Poll::Pending
        }
    }
}

Timer 구조체를 생성하고 duration 필드를 만든다. Future trait을 Timer에 대해 구현한다.
poll 메서드를 구현 해야한다. 간단한 타이머를 구현하기 위해서 현재 시간을 now에 할당 후 Timer객체의 duration과 비교한다.
만약 duration보다 now가 크다면 설정한 시간을 넘었다고 판단할 수 있으므로 Poll::Ready를 반환해 준다.

let duration = self.duration;

이 부분은 self가 pin으로 래핑 된 Timer객체의 가변 참조이다.
pin은 메모리의 이동을 막는 것이지 읽기/쓰기 자체는 허용한다.
여기서 pin에 대해서 깊이 다루지는 않겠다.

pin으로 래핑 했는지 간단하게 말하면 현재 poll이 호출된 context가 동일한 스레드에서 실행된다는 보장이 없다.
비동기 작업은 여러 스레드에서 실행될 수 있다. 자기 참조를 하는 객체의 경우 소유권이 이동하면서 참조가 무효화되어 런타임에서 에러가 발생할 수 있다. 따라서 pin으로 래핑 하여 고정시켜야 하는 경우에는 메모리 위치가 고정되도록 설정하는 것이다. 이렇게 하면 Future가 안전하게 상태를 유지하며 비동기적으로 동작할 수 있다.

https://doc.rust-lang.org/std/pin/
더 자세하고 정확한 내용은 위 다큐먼트를 참고하면 된다.

            tokio::spawn(async move {
                println!("left time: {:?}", duration - now);
                tokio::time::sleep(Duration::from_millis(500)).await;
                waker.wake();
            });

위 코드를 통해 task를 비동기 런타임의 스케줄러에 등록을 해준다.
500ms 동안 대기 후 waker.wake()를 호출하여 다시 poll을 호출해준다.
여기서 wakerclone()을 통해서 넘겨주어야 소유권의 이동이 생기지 않는다.

이 과정을 반복하게 되어 최종적으로 완료 시 Poll::Ready가 반환하게 되는 구조이다.

결론

간단하게 Timer예제를 통해서 Future trait에 대해서 감을 익힐 수 있었다.
조금은 이해가 갔으니 더 깊이 있게 공부해야겠다.

0개의 댓글