2P by GN⁺ 2일전 | ★ favorite | 댓글 1개
  • 저자는 Zig 언어를 학습하며 AcoustID 인덱스 재작성 프로젝트를 진행하다가, 네트워크 프로그래밍의 한계를 계기로 새로운 접근을 시도함
  • 기존 C++과 Go에서 사용하던 비동기 I/O와 동시성 모델을 Zig에서도 구현하기 위해 자체 라이브러리 개발을 결심함
  • 그 결과, Go 스타일의 동시성 모델을 Zig에 맞게 구현한 Zio 라이브러리를 제작하여, 콜백 없이 동기식처럼 보이는 비동기 코드를 작성 가능하게 함
  • Zio는 비동기 네트워크·파일 I/O, 채널, 동기화 원시, 신호 감시 등을 지원하며, 단일 스레드 모드에서 Go나 Rust의 Tokio보다 빠른 성능을 보임
  • 이 프로젝트는 Zig의 시스템 수준 성능과 현대적 동시성 모델의 결합 가능성을 보여주며, Zig 생태계 확장의 중요한 전환점으로 평가됨

Zig 언어와 초기 동기

  • 저자는 원래 오디오 소프트웨어용 저수준 언어로 설계된 Zig를 관찰해 왔으나, 실질적 필요성을 느끼지 못했음
    • Zig 창시자 Andrew Kelley가 저자의 Chromaprint 알고리듬을 Zig로 재구현한 사례를 보고 관심을 갖게 됨
  • AcoustID의 역인덱스 재작성 프로젝트를 Zig 학습 기회로 삼아 진행, 결과적으로 C++ 버전보다 더 빠르고 확장성 높은 구현을 달성함
  • 그러나 서버 인터페이스 추가 단계에서 비동기 네트워킹 지원 부족 문제에 직면함

기존 접근 방식과 한계

  • 이전 C++ 버전에서는 Qt 프레임워크를 사용해 비동기 I/O를 처리, 콜백 기반이지만 풍부한 지원 덕분에 사용 가능했음
  • 이후 프로토타입에서는 Go 언어의 네트워킹과 동시성 편의성을 활용했으나, Zig에서는 유사한 수준의 추상화가 부재함
  • Zig로 TCP 서버와 클러스터 계층을 구현하려면 스레드 다수를 생성해야 하는 비효율이 발생
    • 이를 해결하기 위해 NATS 메시징 시스템용 Zig 클라이언트(nats.zig) 를 직접 작성하며 Zig의 네트워킹 기능을 심층적으로 탐구함

Zio 라이브러리의 등장

  • 이러한 경험을 바탕으로 Zio: Zig용 비동기 I/O 및 동시성 라이브러리를 공개함
  • Zio는 콜백 없는 비동기 코드 작성을 목표로 하며, 내부적으로는 비동기 I/O가 동작하지만 외부에서는 동기식처럼 보이는 구조를 가짐
  • Go 스타일의 동시성 모델을 Zig에 맞게 제한적으로 구현
    • Zio의 태스크는 고정 크기 스택을 가진 스택풀 코루틴 형태
    • stream.read() 호출 시 I/O 작업이 백그라운드에서 수행되고, 완료 시 태스크가 재개되어 결과 반환
  • 이 방식은 상태 관리 단순화코드 가독성 향상을 동시에 제공

기능 구성과 런타임 구조

  • Zio는 완전한 비동기 네트워크 및 파일 I/O, 동기화 원시(mutex, 조건 변수 등) , Go 스타일 채널, OS 신호 감시 등을 지원
  • 태스크는 단일 스레드 또는 다중 스레드 모드에서 실행 가능
    • 다중 스레드 모드에서는 태스크가 스레드 간 이동 가능, 지연 시간 감소와 부하 균형 향상 효과
  • 표준 Reader/Writer 인터페이스를 구현해, 외부 라이브러리와의 호환성 확보

성능 및 비교

  • 저자는 아직 공식 벤치마크를 공개하지 않았으나, 단일 스레드 모드에서 Go와 Rust의 Tokio보다 빠른 성능을 확인했다고 언급
  • 컨텍스트 스위칭 비용이 함수 호출 수준으로 낮음, 사실상 무료에 가까운 전환 속도 제공
  • 다중 스레드 모드는 아직 Go/Tokio만큼 견고하지 않지만, 비슷하거나 약간 더 빠른 성능을 보임
    • 향후 공정성(fairness) 기능 추가 시 성능이 일부 감소할 가능성 있음

예시 코드와 활용

  • 문서에는 Zio 기반 HTTP 서버 예시 코드가 포함되어 있음
    • zio.net.Stream을 이용해 연결을 수락하고, 각 연결을 별도 태스크로 처리
    • zio.Runtime이 태스크 실행과 I/O 스케줄링을 관리
  • 이 구조는 비동기 I/O를 동기식 코드처럼 작성할 수 있게 하며, 명확한 흐름 제어와 자원 해제 관리를 가능하게 함

향후 계획과 의의

  • 저자는 Zio를 통해 Zig가 단순한 고성능 시스템 코드용 언어를 넘어, 완전한 네트워크 애플리케이션 개발 언어로 발전할 수 있음을 확인
  • 다음 단계로 NATS 클라이언트를 Zio 기반으로 재작성하고, Zio 기반 HTTP 클라이언트/서버 라이브러리 개발을 계획 중
  • 이 프로젝트는 Zig 생태계의 네트워킹·동시성 인프라 확장을 주도하며, Go나 Rust에 비견되는 현대적 런타임 모델 구축 시도로 평가됨
Hacker News 의견
  • 컨텍스트 스위칭이 함수 호출 수준으로 거의 무료라고 하지만, 실제로는 분기 예측기(branch predictor)가 깨지는 등의 미묘한 비용이 있음
    Zig의 async 설계가 하드웨어 call/return 쌍을 사용하는지, 아니면 간접 점프 기반으로 번역되는지 확실하지 않음
    완벽한 벤치마크를 하려면 두 작업 간 지속적인 스위칭이 있는 프로그램과 완전히 동기적인 프로그램의 총 실행 시간을 비교해야 함. 이건 꽤 까다로운 일임
    • 스택리스 코루틴에서 호출 스택의 맨 아래에서 두 작업을 계속 전환하고, 스택 전환 코드가 인라인되어 있다면 call/ret 불일치 패널티를 대부분 피할 수 있음
      컴파일러를 제어할 수 있다면, I/O 코드의 call/ret을 명시적 점프로 바꾸는 것도 가능함
      장기적으로는 CPU가 스택풀 코루틴을 더 잘 예측하도록 메타 예측기(meta-predictor) 를 도입하길 바람
    • Zig에는 현재 async가 언어 차원에서 사라졌고, OP가 직접 유저 공간에서 태스크 스위칭을 구현한 것임
    • 단순한 코루틴 간 ping-pong 테스트를 했을 때, 다른 솔루션과 비교해 믿기 어려운 수치를 얻은 적이 있음
    • Zig에 곧 새로운 async가 추가될 예정이라, 본격적으로 파기 전에 기다리는 중임
      관련 글: Zig new async I/O
  • Stackful 코루틴은 RAM이 충분할 때 의미가 있음
    나는 Zig를 임베디드(ARM Cortex-M4, 256KB RAM) 환경에서 사용 중이며, C와의 인터롭에서 메모리 안전성을 확보하기 위해 씀
    Rust처럼 색이 있는 async를 더 선호함. 동기 코드처럼 보이는 마법 같은 느낌이 좋지만, 큰 코드베이스에서는 어떤 함수가 blocking인지 구분하기 어려워지는 게 문제임
    • 사실 모든 동기 코드는 소프트웨어가 만든 환상(illusion)
      CPU는 I/O에서 실제로 블록되지 않으며, OS 스레드 자체가 OS가 구현한 스택풀 코루틴임
      언어 차원에서 이 환상을 더 효율적으로 구현할 수 있을 뿐, 본질은 동일함
    • 새로운 Zig IO는 Rust보다 더 세련된 방식으로 색을 입힌(colored) 구조가 될 예정임
      함수가 I/O를 수행하는지 여부에 따라 색이 결정되고, 호출 시점에서 async 여부를 명시함
      Zig는 함수 호출 시 필요한 스택 크기를 계산하는 기능도 목표로 하고 있어, 스택풀 코루틴의 RAM 낭비 문제를 줄일 수 있을 것으로 기대함
    • 그래서 Zig가 I/O를 명시적으로 표현하려는 이유가 바로, 어떤 함수가 blocking인지 추적할 수 있게 하려는 것임
  • Zig를 지금 도입하기엔 시기상조라는 의견이 있음. I/O 모델이 크게 바뀌는 중이라 몇 년은 걸릴 거라는 인상임
    • 나도 2020년에 비슷한 이유로 Zig를 떠났음.
      하지만 프로젝트는 여전히 활발하고, 빠른 출시보다 올바른 설계를 우선시하는 점은 긍정적으로 봄
      지금은 Go나 C를 쓰며 1.0을 기다리는 중임
    • 몇 년은 금방 지나감. Zig는 이미 충분히 쓸 만한 언어임. 쓸 사람은 쓰고, 아닐 사람은 안 씀
    • 실제로는 나쁜 시기가 맞음. 0.16에서 큰 I/O 변경이 예정되어 있고, 저자조차 최신 기능을 아직 사용하지 않음
      나도 I/O 중심 작업을 위해 0.16을 기다릴 예정임
    • 하지만 I/O 관련 작업이라면 Zig 0.15의 버퍼드 리더/라이터 인터페이스를 사용하면 큰 변화는 없을 것임
    • 오히려 지금이 틀렸다고 봄. Zig는 언어 자체가 급변하는 게 아니라, 새로운 강력한 std.Io API를 추가하는 중임
      기존 코드는 그대로 동작하고, 새 API는 더 인체공학적이고 성능이 좋음
      나도 기존 프로젝트를 새 Reader/Writer API로 옮기며 코드가 훨씬 깔끔해졌음
  • 콜백 기반 async가 표준이 된 이유가 여전히 의문
    libtask 같은 접근이 훨씬 깔끔해 보임
    Rust도 콜백 기반 async를 채택했는데, 이유를 잘 모르겠음
    참고: libtask
    • 스택리스 코루틴은 언어 내부에서 구현 가능하고, 기존 기능과의 예측 가능한 상호작용이 장점임
      하지만 스택을 직접 다루면 예외 처리, GC, 디버거 등과 충돌할 수 있음
      LLVM 수준에서 이런 변경을 병합하기도 어렵기 때문에, 언어 설계자 입장에서는 현실적인 제약이 많음
    • Microsoft가 C++ 표준용으로 연구한 결과, 스택리스 코루틴이 메모리 오버헤드가 훨씬 적고, 실행기 설계의 자유도가 높다는 결론이 나왔음
    • zio나 libtask 방식의 단점은 스택 크기를 직접 추정해야 한다는 점
      너무 작으면 오버플로, 너무 크면 메모리 낭비임
      플랫폼마다 필요한 스택 크기가 달라서 이식성 문제도 있음
      Zig 이슈 #157이 해결되면 이 접근이 더 나아질 것 같음
    • libtask 같은 경우 스레드 스택 크기가 모호하고, 일반적인 async 상태보다 훨씬 큼
    • Rust의 async는 콜백이 아니라 폴링(polling) 기반임
      즉, async를 구현하는 세 가지 방식이 있음
      1. 콜백 기반 (Node.js, Swift)
      2. 스택풀 기반 (Go, libtask)
      3. 폴링 기반 (Rust)
        Rust는 정적 상태 머신으로 변환되어 런타임이 폴링함
        스택풀은 메모리 낭비가 크고, 스택 크기 관리가 어려움
        Rust는 이를 피하기 위해 스택리스 구조를 채택했고, Zig는 두 방식을 모두 선택할 수 있게 할 예정임
        참고: zio coroutine 코드
  • TCP 읽기가 한 달 동안 블록될 수도 있는데, I/O 타임아웃 인터페이스는 어떻게 되는지 궁금함
    • TCP 소켓에서 setsockopt로 읽기/쓰기 타임아웃을 설정할 수 있음
      Zig는 POSIX API 레이어를 제공함
      참고: setsockopt 문서
    • 현재 Zig의 std.Io.Reader는 타임아웃을 인식하지 못함
      Python의 asyncio.timeout처럼 동작하는 구조를 구상 중임
      예시 코드:
      var timeout: zio.Timeout = .init;
      defer timeout.cancel(rt);
      timeout.set(rt, 10);
      const n = try reader.interface.readVec(&data);
      
    • 대부분의 async 프레임워크가 타임아웃과 취소를 간과함
      사실 그게 가장 어려운 부분임
  • Scala에는 이미 ZIO라는 동시성 라이브러리가 있음
    참고: zio.dev
  • 최근 Rust의 Tokio에 감명받았는데, Zig에서도 Go 스타일 동시성을 GC 없이 구현할 수 있다면 꼭 써보고 싶음
    • Go는 무한히 확장 가능한 스택 같은 트릭을 GC 덕분에 쓸 수 있음
      하지만 Zig는 저수준 언어임에도 불구하고 고수준 API를 깔끔하게 표현할 수 있어서 인상적이었음
  • Zig를 처음 알게 된 건 Bun 웹사이트였음. 요즘 정말 빠르게 발전 중임
  • 예전 C++ 버전에서는 Qt로 비동기 I/O를 구현했는데, 이번엔 Go로 전환했음
    Zig와 Go 모두에 Qt 바인딩이 새로 생겼음
    • Go: miqt
    • Zig: libqt6zig
      나는 Rust용 바인딩을 원함. cxx-qt가 유일하게 유지되는 프로젝트지만, QML이나 CMake는 쓰고 싶지 않음. Rust + Cargo만으로 Qt를 쓰고 싶음
  • Scala에도 이미 유명한 프레임워크 ZIO가 있어서, 이름 짓기가 참 어렵다는 생각이 듦