-
비동기와 동시성은 자주 혼동되는 개념이지만, 서로 다른 의미를 가짐
-
비동기는 작업들이 순서에 상관없이 실행될 수 있는 가능성을 뜻함
-
동시성은 시스템이 여러 작업을 동시에 진행할 수 있는 능력을 의미함
- 언어 및 라이브러리 생태계에서 두 개념의 명확한 구분 부재로 비효율성과 복잡성이 발생함
-
Zig 언어에서는 비동기와 동시성의 분리를 통해 코드 중복 없이 동기 및 비동기 코드의 공존이 가능함
서론: 비동기와 동시성의 구별 필요성
Rob Pike의 유명한 발표로 '동시성은 병렬성이 아니다'라는 문장이 잘 알려져 있지만, 이보다 실질적으로 중요한 논점이 존재함. 바로 '비동기'라는 개념의 필요성임. 위키백과 정의에 따르면,
-
동시성: 시스템이 동시에 여러 작업을 시간 분할 또는 병렬적으로 처리할 수 있는 능력
-
병렬 컴퓨팅: 실제 물리적 수준에서 여러 작업을 동시 실행하는 것
이 외에, 우리가 놓치고 있는 중요한 개념이 바로 '비동기'임.
예시 1: 두 파일 저장
두 파일(A, B)을 저장할 때 순서가 상관없다면,
io.async(saveFileA, .{io})
io.async(saveFileB, .{io})
- A를 먼저 저장하거나 B를 먼저 저장해도 무방하며, 중간에 번갈아가며 저장해도 문제가 없음
- 심지어 A 파일을 다 저장한 후 B 파일을 시작해도 코드상으로는 올바름
예시 2: 두 소켓 (서버, 클라이언트)
동일 프로그램 내에서 TCP 서버를 만들고 클라이언트를 연결해야 할 때,
io.async(Server.accept, .{server, io})
io.async(Client.connect, .{client, io})
- 이 경우에는 두 작업의 실행이 반드시 겹쳐서 진행될 필요가 있음
- 즉, 서버가 연결을 받아들이는 동안에 클라이언트도 연결을 시도해야 함
- 첫 번째 파일 예시처럼 직렬로 처리하면 의도된 동작이 나오지 않음
개념 정리
비동기, 동시성, 병렬성의 개념을 다음과 같이 정의함
-
비동기(asynchrony) : 작업들이 순서를 벗어나 실행되어도 올바른 결과가 나오는 성질
-
동시성(concurrency) : 병렬적이든 분할 실행이든 여러 작업을 동시에 전개할 수 있는 능력
-
병렬성(parallelism) : 물리적으로 여러 작업이 실시간으로 동시에 실행되는 능력
파일 저장과 소켓 연결 두 예시는 모두 비동기적이지만, 두 번째(서버-클라이언트)는 동시성이 반드시 필요
비동기와 동시성 구분의 실익
이 구분을 하지 않으면 다음과 같은 문제가 초래됨
- 라이브러리 제작자들은 비동기/동기 버전의 코드를 두 번 짜야 함 (예: redis-py vs asyncio-redis)
- 사용자는 비동기 코드가 '전염성'을 띠어, 단 하나의 비동기 라이브러리 의존성만 있어도 전체 프로젝트를 비동기로 바꾸어야 하는 불편함이 발생
- 이를 피하고자 편법적인 우회가 생기고, 이는 종종 *데드락(deadlock)* 과 비효율을 유발함
따라서, 두 개념의 명확한 분리는 라이브러리와 사용자 모두에게 큰 이점을 제공함
Zig: 비동기와 동시성의 분리
Zig 언어는 io.async를 통해 비동기를 사용하지만, 이는 동시성을 보장하지 않음
- 즉, io.async를 써도 내부적으로는 싱글 스레드, 블로킹 모드로 실행 가능
- 예를 들어
io.async(saveFileA, .{io})
io.async(saveFileB, .{io})
이 코드는 블로킹 환경에서
saveFileA(io)
saveFileB(io)
와 동일하게 동작할 수 있음
- 즉, 라이브러리 제작자가 io.async를 사용해도 사용자는 원하면 순차적 블로킹 IO로 실행할 수 있는 유연성을 확보
동시성 도입과 작업 전환(스케줄링) 메커니즘
동시성이 필요한 경우, 실제 효과적 동작을 위해서는
- 블로킹이 아닌 이벤트 기반 IO (epoll, io_uring 등) 사용
- 작업 전환(스위칭) 프리미티브(예: yield) 사용 필요
- 예시로, Zig는 그린스레드 환경에서 스택 스와핑 기법을 사용해 작업 전환을 수행
- OS 수준의 스레드 스케줄링과 유사하게, CPU 레지스터와 스택 등 상태를 저장/복원하여 여러 태스크 간 전환 진행
- 이러한 전환 메커니즘이 있어야 비동기 코드를 실제 동시적으로 스케줄할 수 있음
- 스택리스 코루틴 구현(예: suspend, resume)도 동일 원리임
동기 코드와 비동기 코드의 공존
아래처럼 두 번의 saveData를 io.async로 실행하면,
io.async(saveData, .{io, "a", "b"})
io.async(saveData, .{io, "c", "d"})
- 두 작업이 서로 비동기적이므로, 내부적으로 동기적으로 작성된 함수여도 자연스럽게 작업들을 동시성 컨텍스트에서 스케줄 가능
- 사용자나 라이브러리 제작자가 코드 중복 없이 동기/비동기 함수를 함께 써도 문제가 없음
동시성이 '필수'인 상황 명시하기
특정 함수(예: TCP 서버의 accept)는 실행 시 명시적으로 동시성이 필요함을 코드에 표현할 필요가 있음
- 이를 Zig에서는 io.asyncConcurrent 등 명시적 함수로 구분
- 이러한 방식은 해당 작업이 실행 환경에서 동시성을 지원하지 않으면 에러를 발생시켜줌
- 비동기 목적의 io.async와 달리, 동시성 보장이 필수이므로 failable 함수로 구현됨
결론
-
비동기와 동시성은 전혀 다른 개념이며, 명확히 구분해야 함
- 동기 코드와 비동기 코드를 공존시키는 것이 가능함
- Zig의 비동기/동시성 모델은 코드 중복 없이 두 세계를 함께 활용하게 해줌
- 이러한 구조는 Go 등의 다른 언어에도 적용된 바 있으며, async/await의 전염성을 극복할 수 있는 길 제시
- Zig의 새로운 async I/O 설계를 통해, 앞으로 더욱 직관적인 동시성/비동기 프로그래밍 환경 기대 가능