제목이 좀 과장됐다는 데 동의하지만, 본문은 잘 쓰였고 요지도 잘 전달됨
아직 Rust async에 강한 의견을 낼 만큼 경험이 많지는 않지만, 몇 가지는 눈에 띄었음
좋은 점은 명시적 런타임을 둘 수 있다는 것임. 프로젝트 전체를 async로 오염시키는 대신, 기본은 동기식으로 두고 입출력 “경계”에서만 런타임을 쓸 수 있음
작업 중인 프로젝트에는 이 방식이 잘 맞았고, Zig가 입출력 코드에서 취하는 전략과도 꽤 비슷해 보임. 이 경우에는 함수 색깔 문제도 대부분 해결됐고, 입출력과 CPU 중심 코드를 엄격히 분리해야 했기 때문에 명시적 입출력 런타임이 자연스러웠음
나쁜 점은 생태계 전체가 tokio에 너무 의존하는 것처럼 보인다는 점임. Java의 GC가 선택 사항인데 실제로는 모두 같은 서드파티 GC 런타임을 쓰고, 어떤 라이브러리를 가져와도 그 런타임을 강제당하는 것과 비슷함. 이런 중앙 의존성은 건강하지 않음
맥락에 따라 생태계 전체가 tokio에 의존하는 것처럼 보일 수 있지만, 임베디드 Rust를 보면 좀 더 납득됨
워크스테이션 프로세서에서의 async 런타임 요구사항과 RP2040 같은 환경의 요구사항은 매우 다름. 그래도 백엔드를 바꿀 수 있기 때문에 작은 ARM M0 마이크로컨트롤러용 async 입출력 코드를 작성할 때도, embedded 중심 런타임인 embassy를 쓰면 다른 환경에서 쓰는 코드와 거의 비슷하게 보임
같은 trait와 인터페이스를 쓰기 때문에 런타임 세부사항에 덜 신경 쓸 수 있음. 작은 RTOS를 쓰거나 직접 async 환경을 만드는 것과 비교하면 꽤 좋음
embassy에서 async 코드를 쓰며 배운 내용도 다른 영역으로 가져갈 수 있음
대안이 뭔지 궁금함. tokio를 쓰는 데 만족하지만, 다른 사람들이 smol, async-std, glommio 같은 다른 실행기를 쓰는 것도 좋음
tokio가 표준 라이브러리 일부는 아니어도 잘 유지관리되고 있으니 지금 상황은 괜찮아 보임. 오히려 표준 라이브러리에 들어가면 다른 실행기를 쓰기 어려워지고, 표준 라이브러리를 다른 플랫폼으로 이식하기도 더 어려워질까 걱정됨
물론 이 걱정이 근거 없을 수도 있음
Java를 언급하니 흥미로운데, Java도 역사적으로 비슷한 문제를 겪어왔음
로깅은 지금은 slf4j로 정리됐지만 여전히 다른 걸 쓰는 라이브러리가 있고, 공통 유틸리티는 처음엔 Apache Commons였다가 지금은 Guava가 많음
JSON은 Jackson으로 어느 정도 정리됐지만 Gson이나 Simple-json도 흔하고, null 허용성 애너테이션도 공식화되지 못한 JSR-305의 비공식 배포본에서 checker framework를 거쳐 최근엔 JSpecify로 이동 중임
이런 기본 요소들은 언어가 제공해야 파편화와 사실상 표준 라이브러리의 난립을 피할 수 있음
async를 쓰면서도 tokio에 의존하지 않고 Rust를 활용할 수 있는 영역은 많음. 사실 완전히 tokio에 묶인 건 웹/서버 쪽에 가까워 보임
라이브러리를 실행기 독립적으로 작성하는 건 아주 어렵진 않지만, 꾸준한 주의가 필요하고 커뮤니티 대부분에서 항상 지켜지는 건 아님
훌륭한 글임. 이런 최적화 심층 분석을 좋아하고, 프로젝트 목표도 잘 풀리길 바람
컴파일러가 “사소한” 경우의 최적화에는 종종 큰 노력을 들이지 않는다고 느낀 적이 있음
다만 제목은 내용에 비해 너무 극적임. “Async Rust Optimizations the Compiler Still Misses”였어도 클릭했을 것 같음
제목은 단순히 사실이라서 그렇게 골랐음. async가 2019년쯤 들어온 이후로 크게 달라진 게 많지 않음
이제 trait와 클로저에서 async를 쓸 수는 있지만, 그건 타입 시스템 업데이트이지 async 기계 자체의 변화는 아님. Waker도 조금 다루기 쉬워졌지만 std/core 쪽 개선에 가까움
이해하기로는 async Rust를 랜딩시킨 사람들이 꽤 번아웃을 겪고 활동이 줄었고, 그 뒤를 이어받은 사람이 거의 없음. 다만 Google 쪽 사람들이 캡처된 변수의 메모리 배치를 최적화하는 PR 하나를 열어둔 건 꽤 반가움
나와 동료들은 async를 많이 쓰기 때문에, 어쩌면 직접 하거나 최소한 시작해야 할지도 모르겠음. “공짜”가 강아지를 키우는 것처럼 공짜라는 의미에 가까운 듯함
그래서 제목이 약간 낚시성인 건 맞지만, 그래도 그 제목을 철회할 생각은 없음
제목이 너무 과장됐다는 데 동의함
작성자는 사소한 함수의 오버헤드에 집착하는 것처럼 보임. “패닉”과 “반환됨” 상태의 오버헤드를 불편해하는데, 그건 큰 문제가 아님
유용한 async 블록 대부분은 충분히 커서 오류 케이스 오버헤드가 묻힘
인라이닝 부족에 대해서는 일리가 있을 수 있음. 하지만 대량의 활동 수를 제한하는 건 대체로 각 활동이 요구하는 상태 공간임
async는 전반적으로 덜 익은 아이디어처럼 보임. 일반 코드도 이미 비동기였음
async 작업을 기다려야 하면 스레드가 준비될 때까지 잠들고, 커널이 이를 추상화해 줌. 그런데 논리적 스레드로 코드를 구성하는 걸 싫어해서 이벤트용 콜백 시스템을 추가했고, 이후 콜백은 추론하기 어렵고 순차 제어가 더 낫다는 걸 깨달음
그래서 스레드가 올바른 프로그래밍 모델이었다고 봄
이제 언어 런타임은 이식성과 성능 때문에 “그린 스레드”를 선호하지만, 대부분의 언어는 이를 제대로 제공하지 않음. 대신 async/non-async 색깔 문제, 스케줄링, 우선순위, 비선점 같은 문제들이 생김. 1970년대보다 더 나쁜 스케줄링과 프로세스 모델임
“일반 코드도 이미 async이고, 기다릴 때 스레드가 잠들며 커널이 추상화한다”는 말은 정확하지 않음
async 코드도 종종 표현 가능한 동시성을 최대화하지 못하는 방식으로 작성됨. 예를 들어 “N개의 입출력 작업을 모두 동시에 수행하라”가 아니라 “작업 X마다 await process(x)”처럼 짜는 식임
하지만 스레드 세계에서는 이 동시성 문제가 더 심해짐. 스레드는 본질적으로 너무 무거워서 동시성을 효율적으로 표현하기 어렵고, 그 방향으로 최적화할 방법도 없음
이는 새로운 교훈이 아님. 작업 훔치기 실행기는 전통적 스레드보다 지연 시간이 훨씬 낮고 P99도 더 일관적이라는 사실이 오래전부터 알려져 있었음. 2000년대 초 Apple이 GCD를 만든 이유도 여기에 있음
스레드는 커널 스케줄러가 작업 부하를 이해하는 데 필요한 더 풍부한 정보를 제공하지 못하고, 커널 스레드는 세밀한 동시성을 얻기엔 지나치게 무거운 메커니즘임. 순수 계산이 아니라 입출력이나 혼합 부하일 때는 더 나쁨
모든 프로그램에 이 수준의 성능이 필요한 건 아니지만, 같은 노력으로 더 높은 성능 기준을 달성하기가 훨씬 쉽고, 실제로 전통적 접근이 따라가기 어려운 지연 시간과 처리량을 얻을 수 있음
async가 방향상 맞다는 신호는 io_uring에서도 보임. 커널의 고성능 입출력 접근 방식인 io_uring은 전통적 스레딩과 시스템 호출과는 전혀 다르고, 완료 처리도 async 동시성에 훨씬 가까움. 다만 async/await만으로는 async 작업 간 관계를 표현하기엔 색깔 수가 부족해서 완전히 활용하기가 더 어렵긴 함
커널과 OS 스케줄러가 끼어드는 순간, 원래 가능해야 할 속도보다 3~4자릿수 느려질 수 있음
마지막으로 코루틴/스케줄링 코드를 다뤘을 때, 즉시 종료되는 스레드를 만들고 join하는 데 약 200µs가 걸렸고, 자체 그린 스레드를 만들고 스케줄링한 뒤 기다리는 데는 약 400ns가 걸렸음
누군가 또 absurd하게 복잡한 async 프레임워크를 설계할 때까지 10년 기다릴 필요는 없음. 어떤 시스템 언어에서든 어셈블리 20줄이면 직접 그린 스레드/스택 있는 코루틴을 만들 수 있음
“스레드가 올바른 프로그래밍 모델”인지는 무엇을 하느냐에 달림. 계산 중심 작업에는 스레드가 맞고, 대역폭 중심 작업에는 async가 맞음
대역폭 중심 코드 최적화는 스케줄 설계의 문제임. 고전적인 멀티스레딩 모델에서는 스케줄링을 제한적으로만 제어할 수 있지만, async 모델에서는 거의 완벽하게 제어할 수 있음
잘 최적화된 async 스케줄은 같은 대역폭 중심 작업에서 동등한 멀티스레드 아키텍처보다 훨씬 빠르고, 비교가 안 될 정도임
오늘날 고성능 코드 대부분은 대역폭 중심이고, async는 이런 작업 부하를 더 쉽게 최적화하기 위해 존재함
콜백이 오히려 추론하기 더 쉽다고 봄
동시 처리를 테스트하고 경쟁 조건을 제대로 처리하는지 확인할 때, 콜백은 스케줄링을 제어할 수 있어서 훨씬 쉽다. 각 콜백이 분리된 단위를 나타내므로 어떤 이벤트를 재정렬할 수 있는지 볼 수 있고, 다양한 순서를 더 쉽게 검토할 수 있음
반면 스레드에서는 순서를 무시하기 쉽고, 다른 스레드에서 발생하는 복잡성이 현재 스레드에 언제 영향을 줄 수 있는지 생각하지 않게 됨. 단순한 게 아니라 단순화한 것에 가까움
또 인위적 장벽을 넣어 스레드를 멈추거나, 입출력을 스텁으로 바꿔 순서를 제어하는 콜백이 달린 mock을 넘기지 않는 한 동시 시나리오를 실제로 바꿔 테스트하기 어렵다
콜백의 문제는 캡처된 호출 스택이 논리적 호출 스택이 아니라는 점임. 호출 스택을 의미 있게 만들기 위해 노력한 일부 라이브러리/런타임이 아니면 좋은 오류 정의가 필요함
물론 두 패러다임을 섞어서 양쪽의 단점만 얻을 수도 있음
스레드는 async+콜백보다 더 낫거나 더 나쁜 것이 아니라 다른 모델임. 스레드에 잘 맞는 문제도 있고, async로 표현하는 게 훨씬 나은 문제도 있음
Rust의 주된 목표가 안전성이라면 왜 panic이 있는지 이해가 안 됨. 코드에 절대 panic 가능한 경로가 없다는 걸 증명할 수 있어야 함
이번 주 내내 살펴봤는데, 절대 panic하지 않음을 보장하는 프로그램을 만드는 건 매우 어려움. 이해하기로 panic 핸들러가 약 300KB이고, 이를 제외하는 유일한 방법은 컴파일 시 코드에 panic 가능한 경로가 전혀 없어야 하는 것임. 컴파일 후 바이너리에 panic 핸들러가 포함됐는지 확인하는 방식은 해킹처럼 느껴짐
unwrap과 다른 panic 연산을 lint로 막을 수는 있지만, no-panic Rust 부분집합이 있었다면 이 글에서 다룬 문제의 상당 부분이 사라졌을 것임
실제로는 비트가 뒤집히는 수준이 아니면 발생하지 않을 상황인데도, 이론상 panic할 수 있는 연산이 너무 많은 언어를 다루는 건 답답함. 배열이 비어 있지 않음을 증명하거나 async를 다룰 때도 마찬가지임
결국 절대 일어나지 않을 상황에 대한 오류 처리를 잔뜩 넣거나, 첫 필드와 나머지 리스트를 따로 두는 비어 있지 않은 리스트 패턴 같은 이상한 구조를 쓰게 됨. 그리고 그 구조도 자체적인 부풀림을 추가함
Rust-in-Linux 쪽에서 실패 가능한 메모리 연산 같은 것으로 이 문제를 다루고 있음. 그들에게는 필요한 기능임
배열이 비어 있지 않다는 식의 증명을 포함해, 증명 기반 사용을 늘리는 작업도 천천히 진행 중임
panic은 사용성과 안전성에 꽤 중요함
panic이 없고 모든 상황에서 실행을 계속해야 한다면, 불변식이 깨진 메모리 손상 같은 상황에서 복구하려고 불변식을 검사하는 모든 곳에 오류 처리를 많이 넣어야 함
이는 걱정하고 있는 바로 그 문제, 즉 거의 절대 일어나지 않을 상황을 위한 방대한 오류 처리와 정확히 같은 종류임
Rust의 목표는 메모리 안전성임. panic은 메모리 안전성 측면에서는 완전히 안전함
프로그램을 실행하는 OS조차 완벽하지 않음
도구가 모든 걸 실패 불가능하게 해주길 바라면서 직접 무언가 하려 하지 않는 태도에는 피로감이 큼. 쉬운 API를 원하고, 그게 충분히 쉽지 않으면 YAML로 “프로그래밍”하는 Kubernetes 컨테이너를 원하고, 그것도 쉽지 않으면 GCP나 Amazon의 클릭형 호스팅 서비스를 원하게 됨
결국 프로그래밍이 아니라 실패하지 않는 앱을 소비하고 싶어 하는 태도에 가깝고, 그런 생활 방식은 무언가를 만들어주는 사람들과의 공생 관계 위에 있을 뿐임
이런 못생겼지만 필요한 논의는 C++에서도 한동안 이어져 왔음
Rust에 async가 도입됐을 때부터 전염성 있는 성격이 마음에 들지는 않았음
Rust가 잘되길 바라며, 이런 사람들이 더 많아지면 Rust의 미래도 더 밝아질 수 있음
최근 Rust async 작업을 시작했는데, 지금 겪는 주된 문제는 코드 중복임
비동기 API와 블로킹 API를 모두 지원하고 싶은 함수마다 중복해서 작성해야 함. maybe-async가 있으면 좋을 것 같음
이를 우회하려고 maybe-async, bisync 같은 crate를 살펴봤지만 모두 문제나 강한 제약이 있었음
내 관점에서는 async 함수가 이미 maybe-async임 fn -> void와 fn -> Future의 차이는 전자는 즉시 끝까지 실행되고, 후자는 나중에야 끝날 수 있다는 점임
async 함수를 블로킹 방식으로 실행하고 싶다면 블로킹 실행기를 쓰면 됨
이 글이 마음에 드는 이유는 2026년 Rust 목표까지 보게 해줬기 때문임
팀에서 Rust를 쓰지만 필요한 일을 하기 위해 아주 깊게 들어갈 필요는 없었음. 그래도 커뮤니티 피드백이 많은 언어가 바닥부터 발전하는 모습을 보는 건 즐거움
C++에서는 이런 흐름을 잘 못 느꼈고, 다른 영역에서는 어떻게 돌아가는지도 잘 모름
다만 아쉬운 점은 목표마다 특정 자금 조달이 필요해 보여서 약간 킥스타터 같다는 것임. 지금까지 찾은 최선의 모델이 이건지 궁금함
“프로젝트 목표”라는 용어는 실제 의미에 비해 꽤 오해를 부름
프로젝트 목표는 한 사람이나 소규모 그룹이 어떤 작업을 하고 싶다고 표현하고, Rust 프로젝트 자원봉사자들에게 코드 리뷰나 질문 답변 등 지속적인 지원 시간을 요청하는 시스템임
이것이 Rust 프로젝트 자체가 그 목표를 세웠다거나, 반드시 지지했다는 뜻은 아님
그래서 이를 Rust의 공식 로드맵으로 보는 건 맞지 않고, “이 영역에서 작업하고 싶어 하는 기여자들이 있다” 정도로 보는 게 더 정확함
C++ ISO 위원회 내부에서도 그 언어의 진화 과정이 어느 정도 망가졌다는 합의가 있는 듯함. 주로 규모와 조직 방식 때문임
기술이 상업적으로 자리 잡으면 안타깝게도 이런 식으로 흘러가는 것 같음. 큰 후원자가 자신들이 관심 있는 부분만 후원하는 걸 탓하기는 어려움
다행히 TweedeGolf의 상당한 자금은 네덜란드 정부에서 나오는 것으로 알고 있음
오픈소스 작업에는 대략 두 종류가 있는 것 같음: 기능 개발과 유지보수
새 기능은 “팔” 수 있음. 만들려면 돈이 들지만 실제 문제를 해결하고, 그 문제가 드는 비용이 기능 개발 비용보다 크다면 기업은 보통 돈을 낼 의향이 있음
유지보수는 더 어렵지만, 이제는 유지관리자 펀드도 있음. RustNL의 펀드가 예임: https://rustnl.org/maintainers/
이런 펀드는 더 넓고 지속적인 작업을 대상으로 하며, 여러 조직이 조금씩 기여해서 뒷받침함
최선의 모델인지는 모르겠지만, 적어도 어느 정도는 작동하는 듯함
Rust Async와 Tokio 문서를 읽어보면, CPU 집약적인 부분을 async 스택에 넣으면 안 되는 이유, std::sync::Mutex 같은 기본 도구를 async 블록에서 효율적으로 쓰는 법, 동기 코드와 async 코드를 붙이는 법이 제대로 설명돼 있음
많은 코드는 효율성에 관심이 없거나 필요 없어서 이런 지침을 따르지 않음. 하지만 성능과 효율을 중시하는 프로젝트는 많고, 프로덕션에서 코드가 돌아가면 함정을 깨닫게 됨. ScyllaDB가 한 예임
LLM도 도움이 안 됨. 모든 것을 main까지 async로 생성하고, 잘못된 기본 도구를 쓰며, 시스템을 제대로 설계하지 않음
중복 상태 접기, 즉 process_command 예시처럼 match를 await 분기 밖으로 끌어올리는 패턴은 오늘 기존 async 코드에 누구나 적용할 수 있는 가장 쉬운 방법임
컴파일러 작업이 필요 없고 리팩터링만 하면 됨
최소한 어디에 적용 가능한지 찾아주는 커스텀 lint는 필요할 것임. 그 정도면 컴파일러 작업에 꽤 가까움
“Future는 쉽게 인라이닝되지 않는다”는 부분에 대해, 내가 만든 프로그래밍 언어에서는 async 함수 안의 async 함수 호출을 인라이닝하는 커스텀 패스를 작성했음
대체로 잘 동작하고 일부 보일러플레이트를 없앨 수 있지만, 결과 바이너리 크기가 많이 커짐
기술적으로 Rust도 같은 일을 할 수 있음
Hacker News 의견들
제목이 좀 과장됐다는 데 동의하지만, 본문은 잘 쓰였고 요지도 잘 전달됨
아직 Rust async에 강한 의견을 낼 만큼 경험이 많지는 않지만, 몇 가지는 눈에 띄었음
좋은 점은 명시적 런타임을 둘 수 있다는 것임. 프로젝트 전체를 async로 오염시키는 대신, 기본은 동기식으로 두고 입출력 “경계”에서만 런타임을 쓸 수 있음
작업 중인 프로젝트에는 이 방식이 잘 맞았고, Zig가 입출력 코드에서 취하는 전략과도 꽤 비슷해 보임. 이 경우에는 함수 색깔 문제도 대부분 해결됐고, 입출력과 CPU 중심 코드를 엄격히 분리해야 했기 때문에 명시적 입출력 런타임이 자연스러웠음
나쁜 점은 생태계 전체가 tokio에 너무 의존하는 것처럼 보인다는 점임. Java의 GC가 선택 사항인데 실제로는 모두 같은 서드파티 GC 런타임을 쓰고, 어떤 라이브러리를 가져와도 그 런타임을 강제당하는 것과 비슷함. 이런 중앙 의존성은 건강하지 않음
워크스테이션 프로세서에서의 async 런타임 요구사항과 RP2040 같은 환경의 요구사항은 매우 다름. 그래도 백엔드를 바꿀 수 있기 때문에 작은 ARM M0 마이크로컨트롤러용 async 입출력 코드를 작성할 때도, embedded 중심 런타임인 embassy를 쓰면 다른 환경에서 쓰는 코드와 거의 비슷하게 보임
같은 trait와 인터페이스를 쓰기 때문에 런타임 세부사항에 덜 신경 쓸 수 있음. 작은 RTOS를 쓰거나 직접 async 환경을 만드는 것과 비교하면 꽤 좋음
embassy에서 async 코드를 쓰며 배운 내용도 다른 영역으로 가져갈 수 있음
tokio가 표준 라이브러리 일부는 아니어도 잘 유지관리되고 있으니 지금 상황은 괜찮아 보임. 오히려 표준 라이브러리에 들어가면 다른 실행기를 쓰기 어려워지고, 표준 라이브러리를 다른 플랫폼으로 이식하기도 더 어려워질까 걱정됨
물론 이 걱정이 근거 없을 수도 있음
로깅은 지금은 slf4j로 정리됐지만 여전히 다른 걸 쓰는 라이브러리가 있고, 공통 유틸리티는 처음엔 Apache Commons였다가 지금은 Guava가 많음
JSON은 Jackson으로 어느 정도 정리됐지만 Gson이나 Simple-json도 흔하고, null 허용성 애너테이션도 공식화되지 못한 JSR-305의 비공식 배포본에서 checker framework를 거쳐 최근엔 JSpecify로 이동 중임
이런 기본 요소들은 언어가 제공해야 파편화와 사실상 표준 라이브러리의 난립을 피할 수 있음
라이브러리를 실행기 독립적으로 작성하는 건 아주 어렵진 않지만, 꾸준한 주의가 필요하고 커뮤니티 대부분에서 항상 지켜지는 건 아님
훌륭한 글임. 이런 최적화 심층 분석을 좋아하고, 프로젝트 목표도 잘 풀리길 바람
컴파일러가 “사소한” 경우의 최적화에는 종종 큰 노력을 들이지 않는다고 느낀 적이 있음
다만 제목은 내용에 비해 너무 극적임. “Async Rust Optimizations the Compiler Still Misses”였어도 클릭했을 것 같음
이제 trait와 클로저에서 async를 쓸 수는 있지만, 그건 타입 시스템 업데이트이지 async 기계 자체의 변화는 아님. Waker도 조금 다루기 쉬워졌지만 std/core 쪽 개선에 가까움
이해하기로는 async Rust를 랜딩시킨 사람들이 꽤 번아웃을 겪고 활동이 줄었고, 그 뒤를 이어받은 사람이 거의 없음. 다만 Google 쪽 사람들이 캡처된 변수의 메모리 배치를 최적화하는 PR 하나를 열어둔 건 꽤 반가움
나와 동료들은 async를 많이 쓰기 때문에, 어쩌면 직접 하거나 최소한 시작해야 할지도 모르겠음. “공짜”가 강아지를 키우는 것처럼 공짜라는 의미에 가까운 듯함
그래서 제목이 약간 낚시성인 건 맞지만, 그래도 그 제목을 철회할 생각은 없음
작성자는 사소한 함수의 오버헤드에 집착하는 것처럼 보임. “패닉”과 “반환됨” 상태의 오버헤드를 불편해하는데, 그건 큰 문제가 아님
유용한 async 블록 대부분은 충분히 커서 오류 케이스 오버헤드가 묻힘
인라이닝 부족에 대해서는 일리가 있을 수 있음. 하지만 대량의 활동 수를 제한하는 건 대체로 각 활동이 요구하는 상태 공간임
async는 전반적으로 덜 익은 아이디어처럼 보임. 일반 코드도 이미 비동기였음
async 작업을 기다려야 하면 스레드가 준비될 때까지 잠들고, 커널이 이를 추상화해 줌. 그런데 논리적 스레드로 코드를 구성하는 걸 싫어해서 이벤트용 콜백 시스템을 추가했고, 이후 콜백은 추론하기 어렵고 순차 제어가 더 낫다는 걸 깨달음
그래서 스레드가 올바른 프로그래밍 모델이었다고 봄
이제 언어 런타임은 이식성과 성능 때문에 “그린 스레드”를 선호하지만, 대부분의 언어는 이를 제대로 제공하지 않음. 대신 async/non-async 색깔 문제, 스케줄링, 우선순위, 비선점 같은 문제들이 생김. 1970년대보다 더 나쁜 스케줄링과 프로세스 모델임
async 코드도 종종 표현 가능한 동시성을 최대화하지 못하는 방식으로 작성됨. 예를 들어 “N개의 입출력 작업을 모두 동시에 수행하라”가 아니라 “작업 X마다 await process(x)”처럼 짜는 식임
하지만 스레드 세계에서는 이 동시성 문제가 더 심해짐. 스레드는 본질적으로 너무 무거워서 동시성을 효율적으로 표현하기 어렵고, 그 방향으로 최적화할 방법도 없음
이는 새로운 교훈이 아님. 작업 훔치기 실행기는 전통적 스레드보다 지연 시간이 훨씬 낮고 P99도 더 일관적이라는 사실이 오래전부터 알려져 있었음. 2000년대 초 Apple이 GCD를 만든 이유도 여기에 있음
스레드는 커널 스케줄러가 작업 부하를 이해하는 데 필요한 더 풍부한 정보를 제공하지 못하고, 커널 스레드는 세밀한 동시성을 얻기엔 지나치게 무거운 메커니즘임. 순수 계산이 아니라 입출력이나 혼합 부하일 때는 더 나쁨
모든 프로그램에 이 수준의 성능이 필요한 건 아니지만, 같은 노력으로 더 높은 성능 기준을 달성하기가 훨씬 쉽고, 실제로 전통적 접근이 따라가기 어려운 지연 시간과 처리량을 얻을 수 있음
async가 방향상 맞다는 신호는 io_uring에서도 보임. 커널의 고성능 입출력 접근 방식인 io_uring은 전통적 스레딩과 시스템 호출과는 전혀 다르고, 완료 처리도 async 동시성에 훨씬 가까움. 다만 async/await만으로는 async 작업 간 관계를 표현하기엔 색깔 수가 부족해서 완전히 활용하기가 더 어렵긴 함
마지막으로 코루틴/스케줄링 코드를 다뤘을 때, 즉시 종료되는 스레드를 만들고 join하는 데 약 200µs가 걸렸고, 자체 그린 스레드를 만들고 스케줄링한 뒤 기다리는 데는 약 400ns가 걸렸음
누군가 또 absurd하게 복잡한 async 프레임워크를 설계할 때까지 10년 기다릴 필요는 없음. 어떤 시스템 언어에서든 어셈블리 20줄이면 직접 그린 스레드/스택 있는 코루틴을 만들 수 있음
대역폭 중심 코드 최적화는 스케줄 설계의 문제임. 고전적인 멀티스레딩 모델에서는 스케줄링을 제한적으로만 제어할 수 있지만, async 모델에서는 거의 완벽하게 제어할 수 있음
잘 최적화된 async 스케줄은 같은 대역폭 중심 작업에서 동등한 멀티스레드 아키텍처보다 훨씬 빠르고, 비교가 안 될 정도임
오늘날 고성능 코드 대부분은 대역폭 중심이고, async는 이런 작업 부하를 더 쉽게 최적화하기 위해 존재함
동시 처리를 테스트하고 경쟁 조건을 제대로 처리하는지 확인할 때, 콜백은 스케줄링을 제어할 수 있어서 훨씬 쉽다. 각 콜백이 분리된 단위를 나타내므로 어떤 이벤트를 재정렬할 수 있는지 볼 수 있고, 다양한 순서를 더 쉽게 검토할 수 있음
반면 스레드에서는 순서를 무시하기 쉽고, 다른 스레드에서 발생하는 복잡성이 현재 스레드에 언제 영향을 줄 수 있는지 생각하지 않게 됨. 단순한 게 아니라 단순화한 것에 가까움
또 인위적 장벽을 넣어 스레드를 멈추거나, 입출력을 스텁으로 바꿔 순서를 제어하는 콜백이 달린 mock을 넘기지 않는 한 동시 시나리오를 실제로 바꿔 테스트하기 어렵다
콜백의 문제는 캡처된 호출 스택이 논리적 호출 스택이 아니라는 점임. 호출 스택을 의미 있게 만들기 위해 노력한 일부 라이브러리/런타임이 아니면 좋은 오류 정의가 필요함
물론 두 패러다임을 섞어서 양쪽의 단점만 얻을 수도 있음
Rust의 주된 목표가 안전성이라면 왜 panic이 있는지 이해가 안 됨. 코드에 절대 panic 가능한 경로가 없다는 걸 증명할 수 있어야 함
이번 주 내내 살펴봤는데, 절대 panic하지 않음을 보장하는 프로그램을 만드는 건 매우 어려움. 이해하기로 panic 핸들러가 약 300KB이고, 이를 제외하는 유일한 방법은 컴파일 시 코드에 panic 가능한 경로가 전혀 없어야 하는 것임. 컴파일 후 바이너리에 panic 핸들러가 포함됐는지 확인하는 방식은 해킹처럼 느껴짐
unwrap과 다른 panic 연산을 lint로 막을 수는 있지만, no-panic Rust 부분집합이 있었다면 이 글에서 다룬 문제의 상당 부분이 사라졌을 것임
실제로는 비트가 뒤집히는 수준이 아니면 발생하지 않을 상황인데도, 이론상 panic할 수 있는 연산이 너무 많은 언어를 다루는 건 답답함. 배열이 비어 있지 않음을 증명하거나 async를 다룰 때도 마찬가지임
결국 절대 일어나지 않을 상황에 대한 오류 처리를 잔뜩 넣거나, 첫 필드와 나머지 리스트를 따로 두는 비어 있지 않은 리스트 패턴 같은 이상한 구조를 쓰게 됨. 그리고 그 구조도 자체적인 부풀림을 추가함
배열이 비어 있지 않다는 식의 증명을 포함해, 증명 기반 사용을 늘리는 작업도 천천히 진행 중임
panic이 없고 모든 상황에서 실행을 계속해야 한다면, 불변식이 깨진 메모리 손상 같은 상황에서 복구하려고 불변식을 검사하는 모든 곳에 오류 처리를 많이 넣어야 함
이는 걱정하고 있는 바로 그 문제, 즉 거의 절대 일어나지 않을 상황을 위한 방대한 오류 처리와 정확히 같은 종류임
도구가 모든 걸 실패 불가능하게 해주길 바라면서 직접 무언가 하려 하지 않는 태도에는 피로감이 큼. 쉬운 API를 원하고, 그게 충분히 쉽지 않으면 YAML로 “프로그래밍”하는 Kubernetes 컨테이너를 원하고, 그것도 쉽지 않으면 GCP나 Amazon의 클릭형 호스팅 서비스를 원하게 됨
결국 프로그래밍이 아니라 실패하지 않는 앱을 소비하고 싶어 하는 태도에 가깝고, 그런 생활 방식은 무언가를 만들어주는 사람들과의 공생 관계 위에 있을 뿐임
이런 못생겼지만 필요한 논의는 C++에서도 한동안 이어져 왔음
Rust에 async가 도입됐을 때부터 전염성 있는 성격이 마음에 들지는 않았음
Rust가 잘되길 바라며, 이런 사람들이 더 많아지면 Rust의 미래도 더 밝아질 수 있음
최근 Rust async 작업을 시작했는데, 지금 겪는 주된 문제는 코드 중복임
비동기 API와 블로킹 API를 모두 지원하고 싶은 함수마다 중복해서 작성해야 함.
maybe-async가 있으면 좋을 것 같음이를 우회하려고 maybe-async, bisync 같은 crate를 살펴봤지만 모두 문제나 강한 제약이 있었음
async나const같은 키워드에 대해 함수를 제네릭으로 만들 수 있게 하는 키워드 제네릭 작업이 진행 중임지금 동기/비동기 양쪽에서 살고 싶은 코드를 작성하는 최선의 선택지는 sans-io임. Fireguard의 Thomas Eizinger가 이 패턴에 대한 좋은 글을 썼음[1]
이 패턴은 sync/async 문제를 깔끔하게 해결할 뿐 아니라 테스트도 쉽게 만들고, DST 같은 기법으로 가는 문도 열어줌[2]
이 주제에 대해 내가 쓴 글도 있는데[3], 문제는 async 대 sync를 넘어 서로 다른 실행기까지 포함하는 더 넓은 문제라는 점을 강조함
0: https://github.com/rust-lang/effects-initiative
1: https://www.firezone.dev/blog/sans-io
2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
3: https://hugotunius.se/2024/03/08/on-async-rust.html
async함수가 이미maybe-async임fn -> void와fn -> Future의 차이는 전자는 즉시 끝까지 실행되고, 후자는 나중에야 끝날 수 있다는 점임async 함수를 블로킹 방식으로 실행하고 싶다면 블로킹 실행기를 쓰면 됨
이 글이 마음에 드는 이유는 2026년 Rust 목표까지 보게 해줬기 때문임
팀에서 Rust를 쓰지만 필요한 일을 하기 위해 아주 깊게 들어갈 필요는 없었음. 그래도 커뮤니티 피드백이 많은 언어가 바닥부터 발전하는 모습을 보는 건 즐거움
C++에서는 이런 흐름을 잘 못 느꼈고, 다른 영역에서는 어떻게 돌아가는지도 잘 모름
다만 아쉬운 점은 목표마다 특정 자금 조달이 필요해 보여서 약간 킥스타터 같다는 것임. 지금까지 찾은 최선의 모델이 이건지 궁금함
프로젝트 목표는 한 사람이나 소규모 그룹이 어떤 작업을 하고 싶다고 표현하고, Rust 프로젝트 자원봉사자들에게 코드 리뷰나 질문 답변 등 지속적인 지원 시간을 요청하는 시스템임
이것이 Rust 프로젝트 자체가 그 목표를 세웠다거나, 반드시 지지했다는 뜻은 아님
그래서 이를 Rust의 공식 로드맵으로 보는 건 맞지 않고, “이 영역에서 작업하고 싶어 하는 기여자들이 있다” 정도로 보는 게 더 정확함
기술이 상업적으로 자리 잡으면 안타깝게도 이런 식으로 흘러가는 것 같음. 큰 후원자가 자신들이 관심 있는 부분만 후원하는 걸 탓하기는 어려움
다행히 TweedeGolf의 상당한 자금은 네덜란드 정부에서 나오는 것으로 알고 있음
새 기능은 “팔” 수 있음. 만들려면 돈이 들지만 실제 문제를 해결하고, 그 문제가 드는 비용이 기능 개발 비용보다 크다면 기업은 보통 돈을 낼 의향이 있음
유지보수는 더 어렵지만, 이제는 유지관리자 펀드도 있음. RustNL의 펀드가 예임: https://rustnl.org/maintainers/
이런 펀드는 더 넓고 지속적인 작업을 대상으로 하며, 여러 조직이 조금씩 기여해서 뒷받침함
최선의 모델인지는 모르겠지만, 적어도 어느 정도는 작동하는 듯함
Rust Async와 Tokio 문서를 읽어보면, CPU 집약적인 부분을 async 스택에 넣으면 안 되는 이유,
std::sync::Mutex같은 기본 도구를 async 블록에서 효율적으로 쓰는 법, 동기 코드와 async 코드를 붙이는 법이 제대로 설명돼 있음많은 코드는 효율성에 관심이 없거나 필요 없어서 이런 지침을 따르지 않음. 하지만 성능과 효율을 중시하는 프로젝트는 많고, 프로덕션에서 코드가 돌아가면 함정을 깨닫게 됨. ScyllaDB가 한 예임
LLM도 도움이 안 됨. 모든 것을
main까지 async로 생성하고, 잘못된 기본 도구를 쓰며, 시스템을 제대로 설계하지 않음중복 상태 접기, 즉
process_command예시처럼 match를 await 분기 밖으로 끌어올리는 패턴은 오늘 기존 async 코드에 누구나 적용할 수 있는 가장 쉬운 방법임컴파일러 작업이 필요 없고 리팩터링만 하면 됨
“Future는 쉽게 인라이닝되지 않는다”는 부분에 대해, 내가 만든 프로그래밍 언어에서는 async 함수 안의 async 함수 호출을 인라이닝하는 커스텀 패스를 작성했음
대체로 잘 동작하고 일부 보일러플레이트를 없앨 수 있지만, 결과 바이너리 크기가 많이 커짐
기술적으로 Rust도 같은 일을 할 수 있음