Async Rust: Where does the scheduler live?
비동기 Rust에서 스케줄러는 어디에 있어야 할까요?
비동기 Rust의 복잡성은 스케줄러가 필요한 동시성의 속성이지, Rust만의 결함이 아니라고 설명합니다. 런타임을 라이브러리로 두는 설계의 이점과 비용을 짚고, Zig처럼 I/O 인터페이스를 함수 매개변수로 전달하는 대안을 살펴봅니다.
- 주제
AI 요약
이 글은 비동기 Rust 논쟁에서 반복해서 등장하는 불만을 빙고 카드 형식으로 정리한 뒤, 그중 하나인 “런타임이 왜 필요한가”를 파고듭니다. 비동기 클로저, 함수 색칠(function coloring), Tokio 중심주의, 취소 안전성, Send·Sync·'static 제약, Pin 같은 주제를 늘어놓으며, 비동기 Rust의 불편이 어디서 비롯되는지 살펴봅니다.
동시성에는 스케줄러가 필요합니다
글쓴이는 처음에는 비동기 Rust가 런타임을 요구한다는 점을 불만으로 삼습니다. Rust의 장점이 가비지 컬렉터 같은 런타임 없이 메모리 안전성을 제공하는 데 있는데, 비동기 코드에서 다시 런타임에 의존하면 후퇴처럼 보인다는 주장입니다. 하지만 곧 이 불만을 뒤집습니다. 동시 실행에는 작업을 언제 실행할지 정하는 스케줄러가 필요하며, 스레드를 선택해도 스케줄러가 사라지는 것은 아닙니다. Tokio 대신 운영체제 커널이 스케줄러 역할을 맡을 뿐입니다.
스케줄링에는 하나의 정답이 없습니다. 작업을 코어 사이에서 옮기는 work stealing 방식은 각 스레드가 계속 일하도록 돕지만, 작업에 Send 제약이 붙습니다. 반대로 thread-per-core 런타임은 작업을 다른 코어로 옮기지 않는 대신 다른 제약과 특성을 가집니다. Rust가 런타임을 표준으로 정하지 않고 선택을 사용자에게 맡기는 이유도 서로 다른 사용 사례에 맞는 스케줄러가 필요하기 때문이라고 설명합니다.
런타임을 라이브러리에 둔 대가
Rust의 비동기 런타임은 보통 크레이트로 가져와 초기화합니다. 이 선택은 내장 그린 스레드를 두지 않고, 임베디드와 no_std 환경까지 겨냥하는 Rust의 설계 방향과 맞닿아 있습니다. 언어에 런타임을 내장하면 임베디드 사용이 어려워지고 FFI 경계에도 비용이 생깁니다. 런타임을 라이브러리로 둔 덕분에 사용자가 실행 환경을 고를 수 있지만, 그만큼 Tokio에 코드가 몰리고 런타임 간 호환성이 약해집니다. 런타임이 작업을 스레드 사이에서 옮길지 언어가 미리 알 수 없으므로 Send 같은 제약도 제네릭 코드로 퍼집니다.
성능에 관한 흔한 기대도 바로잡습니다. Future가 필요한 데이터만 담는 최적 크기의 상태 머신으로 컴파일된다고 여기는 사람이 있지만, 글의 예시에서는 wait().await 뒤에 사용되는 8,192바이트 인자가 Future 크기에 두 번 들어갑니다. 기대 크기는 8,194바이트지만 실제 크기는 16,386바이트입니다. Future를 인자로 넘기는 구조에서는 크기 증가가 지수적으로 커질 수 있다고 지적합니다.
Tokio의 암묵적 실행 문맥
글쓴이는 Tokio의 암묵적 실행자(executor)도 불편한 지점으로 꼽습니다. tokio::spawn은 현재 스레드에 Tokio 런타임 문맥이 있다고 전제합니다. 예시에서는 일반 코드에서 호출하면 동작하지만, Rayon 작업 안에서 같은 함수를 호출하면 런타임이 없어 패닉이 발생합니다. 실행자 인자를 명시적으로 전달하지 않아도 되는 편리함 대신, 코드에 드러나지 않는 실행 문맥에 의존하게 된다는 설명입니다. 글쓴이가 선호하는 방식은 작업을 생성하는 지점까지 실행자를 매개변수로 전달하는 것입니다.
Zig의 I/O 인터페이스 제안
대안으로 Zig의 방식을 소개합니다. I/O가 필요한 함수에 Io 인터페이스를 매개변수로 전달하고, 호출자가 구현을 선택합니다. 글쓴이는 이 방식이 어떤 함수에서 블로킹 작업이 일어날지 추적하기 쉽고, 비동기 여부를 별도 언어 문법이 아니라 매개변수로 표현하며, 라이브러리가 특정 실행자에 묶이지 않게 한다고 봅니다. 스케줄러를 커널이나 암묵적 전역 문맥에 두는 대신 명시적으로 전달하면, 구현을 바꾸기도 쉽고 호출 경로도 눈에 보인다는 주장입니다.
다만 Zig는 아직 안정화되지 않았고 비동기 실행은 구현하기 까다롭습니다. 이 접근이 실제 생태계에서 검증됐다고 보기는 어렵습니다. Rust 표준 라이브러리의 std::fs 같은 API를 전면 개편하기도 어렵지만, 크레이트 생태계에서 실험할 여지는 있다고 글쓴이는 덧붙입니다. 전체 논지는 비동기 Rust의 여러 난점이 동시성에 따르는 문제인지, 스케줄러를 라이브러리에 둔 선택에서 파생된 문제인지 나눠 보자는 것입니다.
Lobsters 반응
- @notgull — Tokio가 “숨겨진 런타임 인자”를 전달한다는 지적은 타당합니다. 비동기 런타임을 없애거나 사용자에게 보이지 않게 하려는 시도는 이해를 돕기보다 해를 끼친다고 생각합니다.
- @mond — 저도 결국 그 지점에 도달했습니다. 제 생각이 맞는지는 모르겠지만, 그 부분만큼은 뭔가 마음에 걸립니다.
- @atk — 이 문제는 Tokio만의 특성이 아닙니다. 스레드 없이도 실행할 수 있는 합리적인 비동기 구현이라면, 적어도 비동기 파일 I/O를 하지 않는 동안에는 Rust에서 전역 또는 스레드 로컬 상태가 필요합니다. 이 설계가 훌륭하다는 뜻은 아니지만 대안에도 단점이 많습니다.
- @carlana — 빙고 카드 형식이 정말 웃깁니다.
- @hjvt — “함수 색칠을 피합니다. 매개변수일 뿐입니다”라는 표현에는 문제가 있습니다. 함수 색칠은 함수 매개변수와 동형입니다. 호출 스택 아래쪽에서 X를 매개변수로 요구하면 그 요구가 위로 퍼지는 현상입니다. 매개변수가 암묵적인지 명시적인지는 본질적으로 중요하지 않습니다. 반환형도 마찬가지입니다. 이전까지 실패하지 않던 호출 스택 아래쪽에
Result를 도입하면,unwrap하지 않는 한 위로 퍼집니다.unwrap하면 스택을 되감으니 철학적으로는 여전히 퍼지지만, 암묵적이고 보이지 않는 방식일 뿐입니다. - @easrng — 이 페이지를 열면 Firefox가 충돌하는 건 저뿐인가요?
- @proctrap — 저한테는 괜찮습니다. 당신만 그런 것 같습니다.
- @mond — 그럴 수도 있겠네요. 저는 Firefox를 주로 쓰는데 이런 문제는 없습니다.
원문: Here Comes the Moon / 번역·요약: Trawling