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