Zig 0.16의 std.Io와 zio를 비교하며 오늘 바로 사용할 수 있는 비동기 I/O 구현과 성능 차이를 살펴봅니다.
Zig 0.16는 지난달 std.Io와 함께 출시되었는데, 이는 I/O와 동시성을 위한 크로스플랫폼 인터페이스입니다. 이것은 생태계에 큰 진전입니다. 이제 라이브러리는 런타임과 독립적인 표준 I/O 추상화를 기준으로 작성될 수 있고, 애플리케이션 개발자는 원하는 구현을 연결할 수 있습니다.
0.16과 함께 제공된 유일하게 사용 가능한 구현은 스레드 풀을 사용하는 std.Io.Threaded입니다. 동시 작업을 생성하면, 이를 실행하기 위해 OS 스레드를 만듭니다. 간단한 예제로 어떻게 동작하는지 살펴봅시다:
const std = @import("std");
const num_tasks = 10_000;
fn task(io: std.Io) std.Io.Cancelable!void {
try io.sleep(.fromSeconds(10), .awake);
}
pub fn main(init: std.process.Init) !void {
var group: std.Io.Group = .init;
for (0..num_tasks) |_| {
try group.concurrent(init.io, task, .{init.io});
}
try group.await(init.io);
}
이 코드는 각각 10초 동안 잠드는 10,000개의 동시 작업을 생성합니다. 제 컴퓨터에서는 약 20초 만에 완료됩니다:
$ time ./std_demo
real 0m20.158s
user 0m2.258s
sys 0m10.098s
오버헤드는 OS 스레드를 생성하는 데서 옵니다. 이것을 50,000개 작업으로 늘리려고 하면, 대부분의 시스템에서는 스레드 제한 때문에 아마 실패할 것입니다(Linux에서는 ulimit -u).
이것은 단지 임의의 벤치마크가 아닙니다. 비동기 I/O는 실제 문제를 해결하기 위해 존재합니다. 바로 많은 클라이언트가 연결된 네트워크 서버입니다. 각 클라이언트 연결마다 OS 스레드를 하나씩 생성하고 싶지는 않을 것입니다. 그래서 이벤트 루프, 코루틴, 비동기 I/O가 존재합니다.
표준 라이브러리에는 Linux에서 io_uring, BSD/macOS에서 kqueue를 사용하도록 의도된 std.Io.Evented가 있습니다. 하지만 아직 작업이 진행 중이며, 많은 함수가 빠져 있고 현재는 컴파일도 되지 않습니다.
저는 이전에도 zio에 대해 쓴 적이 있고, 방금 완전한 std.Io 구현을 포함한 0.11 버전을 릴리스했습니다. 이것은 스택풀 코루틴과 비동기 OS 수준 I/O API(Linux에서는 io_uring 또는 epoll, BSD/macOS에서는 kqueue, Windows에서는 IOCP)를 사용합니다. 같은 예제를 zio로 작성하면 다음과 같습니다:
const std = @import("std");
const zio = @import("zio");
const num_tasks = 10_000;
fn task(io: std.Io) std.Io.Cancelable!void {
try io.sleep(.fromSeconds(10), .awake);
}
pub fn main(init: std.process.Init) !void {
const rt = try zio.Runtime.init(init.gpa, .{});
defer rt.deinit();
const io = rt.io();
var group: std.Io.Group = .init;
for (0..num_tasks) |_| {
try group.concurrent(io, task, .{io});
}
try group.await(io);
}
코드는 거의 동일합니다. zio 런타임을 초기화하고 io() 메서드를 사용해 std.Io 인터페이스를 얻기만 하면 됩니다. zio를 사용하면 같은 10,000개 작업이 약 10초 만에 완료됩니다:
$ time ./zio_demo
real 0m10.606s
user 0m3.136s
sys 0m7.126s
모든 작업이 진짜로 동시에 실행되므로 이것이 기대되는 시간입니다. 이것을 50,000개 또는 그 이상의 작업으로 늘려도 계속 동작하며, 제한 요소는 사용 가능한 메모리뿐입니다.
이 io 인스턴스는 std.Io.Threaded에 사용하는 어떤 용도에도 사용할 수 있습니다. 예를 들어 std.http.Server로 HTTP 서버를 작성하려면 zio의 io를 넘기기만 하면 되고, 같은 방식으로 동작합니다.
표준 API로 Zig 0.16에서 비동기 I/O를 사용하고 싶다면 std.Io.Evented가 준비될 때까지 기다릴 필요가 없습니다. Zio의 구현은 아직 새롭기 때문에 문제가 생기면 GitHub에서 알려 주세요. 기꺼이 도와드리겠습니다.