2P by GN⁺ 1일전 | ★ favorite | 댓글 1개
  • Zig 언어가 기존 비동기 I/O 설계의 복잡성을 줄이기 위해 새로운 Io 인터페이스 기반 모델을 도입
  • 이 모델은 동기·비동기 코드의 구분 없이 동일한 함수 구조를 유지하며, Io.ThreadedIo.Evented 두 구현을 제공
  • Io.Threaded는 기본적으로 동기 실행을, Io.Evented이벤트 루프 기반 비동기 실행을 수행
  • 개발자는 async()concurrent() 함수를 통해 병렬 실행 제어가 가능하며, 코드 수정 없이 성능 최적화 가능
  • 이 접근은 함수 색칠(function coloring) 문제를 해결하고, Zig의 단순성과 제어성을 유지한 채 비동기 성능을 확보하는 방향

Zig의 비동기 설계 변화

  • Zig는 기존 비동기 설계가 언어의 미니멀리즘 철학과 잘 맞지 않아 새로운 접근을 모색
    • 기존 설계는 다른 기능과의 통합성이 낮았음
    • 새 모델은 동기·비동기 I/O를 동일한 코드 구조로 처리 가능
  • 새로운 설계는 Io 제너릭 인터페이스를 중심으로 동작
    • 모든 I/O 함수는 Io 인스턴스를 매개변수로 받아 실행
    • Allocator 인터페이스와 유사한 구조로, 메모리 할당과 같은 방식으로 I/O 제어 가능

Io 인터페이스의 구조

  • 표준 라이브러리에 두 가지 기본 구현체 포함
    • Io.Threaded : 기본적으로 동기 실행, 필요 시 스레드 병렬 처리
    • Io.Evented : 이벤트 루프 기반 비동기 실행 (io_uring, kqueue 등 사용)
  • 사용자는 직접 새로운 Io 구현체를 작성할 수 있어, 실행 방식에 대한 세밀한 제어 가능

코드 예시와 동작 방식

  • 예시 함수 saveFile()은 파일 생성, 쓰기, 닫기를 수행
    • Io.Threaded 사용 시 일반 시스템 호출로 동작
    • Io.Evented 사용 시 비동기 백엔드로 실행
    • 두 경우 모두 writeAll() 호출 시점에 작업 완료 보장
  • 동일한 코드가 동기·비동기 환경 모두에서 동일하게 작동
    • 라이브러리 작성자는 실행 방식에 신경 쓸 필요 없음

병렬 실행과 async() / concurrent()

  • async() 함수는 비동기 실행을 요청하지만, Io.Threaded에서는 즉시 실행될 수도 있음
    • Io.Evented에서는 실제 비동기 실행으로 두 파일을 동시에 저장 가능
  • concurrent() 함수는 실제 병렬 실행이 필요한 경우 사용
    • Io.Threaded는 스레드 풀을 활용
    • Io.Eventedasync()와 동일하게 처리
  • 잘못된 함수 선택(async 대신 concurrent)은 버그로 간주, 언어 차원에서 방지 불가

코드 스타일과 언어 통합

  • 비동기 전용 문법 없이 일반 Zig 코드 스타일 유지
    • try, defer 등 기존 제어 흐름 문법 그대로 사용
    • Andrew Kelley는 “표준 Zig 코드처럼 읽힌다”고 언급
  • 예시로 비동기 DNS 조회 구현 제시
    • getaddrinfo()와 달리 첫 번째 성공 응답만 반환하고 나머지 요청은 취소

향후 계획과 개발 현황

  • Io.Evented는 아직 실험적 단계, 일부 OS 미지원
  • WebAssembly 호환 Io 구현이 계획 중이며, 관련 기능 개발 필요
  • Io 관련 24개의 후속 작업 항목이 존재하며 대부분 미완료 상태
  • Zig는 아직 1.0 버전 전으로, 비동기 I/O와 네이티브 코드 생성이 주요 남은 과제
  • 이번 설계로 I/O 인터페이스 변경으로 인한 코드 재작성 빈도 감소 기대

커뮤니티 논의 요약

  • 여러 댓글에서 Zig의 접근이 Rust의 async/await 모델보다 단순하고 유연하다는 평가
    • Rust는 여러 executor 혼용 시 복잡성이 높음
    • Zig는 Io 인터페이스로 다중 executor 공존 가능성 확보
  • 일부는 코드가 다소 장황해질 수 있음을 지적
    • 그러나 명시적 API 설계로 보안·성능·테스트 제어성 향상
  • 비동기 실행과 스레드 실행의 차이, stackful vs stackless coroutine 구현 방식 등 기술적 논의도 이어짐
  • Zig의 Io언어 차원의 특별한 처리 없이 표준 라이브러리 확장 형태로 구현됨
    • 향후 stackless coroutine 기능이 추가될 예정

결론

  • Zig의 새 비동기 모델은 언어 단순성 유지와 고성능 I/O 양립을 목표로 함
  • 함수 색칠 문제 해결, 동기·비동기 코드 통합, 명시적 제어 구조를 통해
    Zig 1.0 안정화의 핵심 단계로 평가됨
Hacker News 의견
  • 전체적으로 이 글은 정확하고 잘 조사된 내용임
    다만 몇 가지 작은 수정 사항이 있음.
    Io.Threaded 인스턴스에서 async()는 실제로 비동기로 동작하지 않고 즉시 실행됨. 하지만 std.Io.Threaded는 기본적으로 스레드 풀을 사용해 비동기 작업을 분배함.
    단, init_single_threaded로 초기화하면 기사에서 설명한 동작처럼 동작함.
    또 하나, 예전에는 asyncConcurrent()라는 함수가 있었지만 지금은 단순히 concurrent()로 이름이 바뀌었음

    • Daroc임. 이 피드백을 반영해 기사에 두 가지 수정 사항을 적용했음.
      앞으로 피드백을 주려면 lwn@lwn.net으로 메일을 보내면 됨.
      수정 제안과 Zig 관련 작업에 감사함
    • Andrew에게 질문이 있음.
      만약 async()를 써야 할 곳에 asyncConcurrent()를 잘못 쓰면 어떤 버그가 생기는지 궁금함.
      IO 모델에 따라 UB(정의되지 않은 동작) 이 될 수도 있는지, 아니면 단순한 논리 오류인지 알고 싶음
    • concurrent()의 좋은 점은 코드의 가독성과 표현력을 높여, “이 코드는 반드시 병렬로 실행돼야 함”을 명확히 보여준다는 점임
  • 이 설계는 꽤 합리적이라고 생각함.
    다만 Zig의 설명은 혼란스러움.
    함수 색칠 문제(function coloring)를 해결했다고 강조하지만, 사실상 효과 타입(effect type) 으로 IO를 밀어 넣은 것에 불과함.
    이는 호출자가 토큰을 유지해야 하는 형태로, 여전히 일종의 색칠임.
    Go의 비동기 처리 방식과 유사하다고 봄

    • 만약 인자만 다르게 호출해도 ‘색칠된 함수’라면, 모든 함수가 색칠된 셈이 되어 의미가 사라짐 ;)
      Zig의 예전 async-await 모델도 이미 coloring 문제를 해결했었음.
      컴파일러가 호출 문맥에 따라 동기/비동기 버전을 자동으로 생성했기 때문임
    • 실제로 function coloring의 핵심 문제는 동기/비동기 코드 경로의 중복임.
      Zig는 이를 의존성 주입으로 해결해 실용적으로 충분함.
      async 호출의 복잡성은 피할 수 없지만, 정밀한 제어를 위해선 어쩔 수 없는 부분임
    • Zig의 io는 전염성 있는 effect type이 아님.
      전역 io 변수를 선언해 어디서든 쓸 수 있음(물론 라이브러리 작성 시엔 권장되지 않음).
      function coloring 문제의 다섯 가지 조건을 정리한 What color is your function? 글을 보면, Zig의 접근이 일부 조건(특히 4, 5)을 만족하지 못할 가능성이 높음
    • 사실상 Zig는 모든 것을 async로 색칠하고, 워커 스레드를 쓸지 말지만 선택하게 한 것 같음.
      하지만 이런 접근은 데드락 같은 문제를 유발할 수 있음.
      일부 코드는 스레드 안전하지 않기 때문에 coloring이 오히려 도움이 됨
    • Haskell 개발자로서 보면, Zig는 언어 지원 없이 IO 모나드를 구현한 셈처럼 보임
  • 이 설계는 Scala의 async와 매우 비슷해 보임.
    Scala에서는 실행 컨텍스트가 암시적 파라미터로 전달되는데, Zig는 명시적으로 받음.
    실제로는 스레드와 큐를 직접 쓰는 것보다 나은 점이 많지 않았고, 실행 컨텍스트 관리가 복잡하고 예측 불가한 동작을 유발했음.
    Zig 팀이 Scala 경험이 적어서 이 접근이 새롭다고 생각한 듯함

    • OS 스레드를 직접 쓰면 Little의 법칙에 따라 확장성 한계에 부딪힘.
      JVM은 가상 스레드로 이를 해결하지만, 저수준 언어는 같은 효율을 내기 어려움.
      따라서 Zig 같은 언어는 다른 방식의 확장성 솔루션이 필요함
    • 참고로 Scala의 ExecutionContext API를 보면 관련 개념을 더 잘 이해할 수 있음
  • 예전 Zig의 async/await 시스템에서는 함수의 suspend/resume이 가능했음.
    이 기능으로 OS 개발 시 디바이스 인터럽트 기반의 프레임 일시 중단/재개를 구현해보고 싶었음.
    새 io 시스템에서는 이걸 직접 구현해야 할 것 같아 아쉬움

    • @asyncSuspend@asyncResume이라는 저수준 빌트인이 존재함.
      새로운 Io는 동기, 스레드, 이벤트 기반의 공통 추상화이므로 suspend 메커니즘은 포함되지 않음
    • 최종적으로는 suspend/resume이 사용자 공간 표준 라이브러리 함수로 구현될 가능성이 있음.
      현재 Io.Evented 프로토타입을 보면, stackless coroutine 기반으로 3rd-party 라이브러리에서 다룰 수도 있음
    • 스레드 풀을 하나만 두고 suspend/resume을 구현할 수 있는지도 궁금함
    • 협력형 코루틴을 선점형 async로 구현하는 게 어떤 의미가 있는지도 의문임
  • 예시 코드에서 writeAll()이 반환될 때 작업이 완료된다고 했는데,
    IO 구현이 다양할 수 있으므로 실제로는 defer가 시작될 때 완료가 보장돼야 함.
    그렇지 않으면 createFilewriteAll 간의 의존 관계 추적이 필요함.
    그렇다면 결국 blocking 호출과 다를 게 없어 보임.
    또한 이 인터페이스 이름이 IO인 이유도 모호함.
    실제로는 “다른 컨텍스트에서 실행”하는 추상화에 더 가까움
    관련 문서: std.Io

  • 다음 예시가 흥미로움

    var a_future = io.async(saveFile, .{io, data, "saveA.txt"});
    var b_future = io.async(saveFile, .{io, data, "saveB.txt"});
    const a_result = a_future.await(io);
    const b_result = b_future.await(io);
    

    Rust나 Python에서는 코루틴이 await되지 않으면 진행되지 않음.
    반면 Zig의 예시는 io.async가 자체적으로 진행된다면, 이는 태스크 생성과 유사함.
    이는 유효한 설계지만 다른 언어들이 택한 방향은 아님

    • C#도 비슷하게 동작함. async 함수가 yield 전까지는 호출 스레드에서 실행
    • Zig에서도 마찬가지로 .await(io)를 호출해야 실행이 보장됨.
      즉시 실행될지, 스레드 풀에 큐잉될지는 Io 런타임 구현에 따라 다름
    • 실제로는 await 시점에 실행이 진행됨.
      evented io의 경우 두 작업이 교차(interleaved) 실행될 수 있고, threaded io에서는 백그라운드에서 진행될 수도 있음.
      즉, “어딘가에서 몰래 실행되는 태스크”는 없음
    • JavaScript도 이런 방식으로 동작함
  • Go를 매일 쓰는 입장에서 Zig의 Io가 Go의 여러 단점을 교정한다고 느낌.
    다만 Zig에 채널(channel) 개념이 있는지 궁금함.
    Go에서는 select 키워드가 있지만 소켓에는 쓸 수 없다는 점이 늘 아쉬웠음

    • 모든 IO를 채널로 감싸면 비용이 크다는 점을 지적함.
      Go의 채널은 수십 사이클 단위의 오버헤드가 있으므로, 작은 단위의 IO에는 비효율적임.
      대신 큰 단위의 데이터 이동이나 다대다 동기화에는 유용함
    • Zig에는 Go의 채널과 유사한 std.Io.Queue가 있음.
      select 문도 비슷하게 구현 가능하지만 문법적으로는 덜 인체공학적(ergonomic) 임.
      대신 GC 없이 다양한 IO 런타임에서 동작할 수 있다는 장점이 있음
    • Odin 언어를 써봤는지 묻고 싶음. Zig보다 Go에서 더 많은 영감을 받은 “better C”임
    • C#의 async/await처럼 색칠된 함수를 강제하지 않은 점이 마음에 듦.
      Zig의 “colorless” 접근이 훨씬 낫다고 생각함
    • Go의 concurrency 모델이 특별하다고 착각하는 건 문제임.
      Goroutine은 그린 스레드, 채널은 스레드 안전 큐일 뿐이며, Zig도 이미 이를 표준 라이브러리로 제공함
  • Zig의 async 버전 Io는 Go의 접근과 거의 동일해 보임.
    다만 Go에서는 C 라이브러리 호출 시 스택 할당 비용이 크고, 직접 syscall은 플랫폼 호환성 문제가 있음.
    Zig는 이를 구성 가능하게 만들어 코드 변경 없이 다양한 트레이드오프를 선택할 수 있게 한 듯함

  • 새 async IO는 단순한 예제에서는 훌륭하지만, 서버 수준의 복잡한 IO에서는 한계가 있을 수 있음.
    관련 이슈를 GitHub에 올려둠

  • 핵심 문제는 언어나 라이브러리 설계자가 서로 다른 실행 컨텍스트(sync/async) 를 연결할 수단을 제공해야 한다는 점임.
    이를 위해 FSM(유한 상태 기계)으로 컨텍스트를 감싸고, 양쪽 간의 통신 채널을 제공하는 방식이 필요함
    관련 글: Function colors represent different execution contexts