Rust의 런타임이 어떻게 작동하는지 이해하기 위해 최소한의 외부 의존성 없이 간단한 런타임을 구현했습니다. 이 런타임은 복잡한 스케줄링 알고리즘 없이 단일 전역 작업 큐를 사용합니다.
다음 코드는 멀티 프로듀서, 멀티 컨슈머(MPMC) 부분만 crossbeam 크레이트를 사용하고 나머지는 직접 작성했습니다.
예제 코드:
fn main() {
let runtime = SimpleRuntime::new();
for i in 1..=20 {
runtime.spawn(async move {
let result = DummyIO::new(Duration::from_secs(i)).await;
println!("{:?}: {:?}", thread::current().id(), result);
});
}
runtime.run(4); // 4개의 스레드 사용
}
DummyIO 구조체도 매우 단순합니다:
pub struct DummyIO {
done: Arc<AtomicBool>,
delay: Duration,
}
impl Future for DummyIO {
type Output = Duration;
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output> {
if self.done.load(Ordering::Acquire) {
return Poll::Ready(self.delay);
}
let done = self.done.clone();
let waker = cx.waker().clone();
let delay = self.delay;
thread::spawn(move || {
thread::sleep(delay);
done.store(true, Ordering::Release);
waker.wake();
});
Poll::Pending
}
}
데이터 구조
다음과 같은 데이터 구조를 사용하였습니다(Tokio의 설계를 참고):
struct Task {
raw: RawTask,
}
unsafe impl Send for Task {}
unsafe impl Sync for Task {}
struct RawTask {
ptr: NonNull<Header>, // Cell<T> 포인터, 여기서 T는 Future
}
struct Header {
state: Mutex<State>,
vtable: &'static Vtable,
sender: crossbeam::channel::Sender<Task>,
}
#[derive(Default)]
struct State {
running: bool,
notified: bool,
completed: bool,
}
#[repr(C)]
struct Cell<T: Future> {
header: Header,
future: T,
output: Option<T::Output>,
}
struct Vtable {
poll_task: unsafe fn(NonNull<Header>),
clone_task: unsafe fn(NonNull<Header>) -> NonNull<Header>,
drop_task: unsafe fn(NonNull<Header>),
}
주요 사항:
RawTask의ptr은 실제로NonNull<Cell<T: Future>>을 가리킵니다.Cell<T: Future>에는#[repr(C)]가 적용되어 있으며, 이에 대한 설명은 주석에 있습니다.vtable의 설계는 Waker의 vtable을 참고하여 타입 정보를 저장하고 나중에 원래 타입으로 복원할 수 있도록 합니다.
GitHub에서 전체 코드를 확인하세요.
즐겁게 코딩하세요!