세 언어 모두 "코루틴"이라는 단어를 쓰지만, 구현 방식은 전혀 다르다.
공통 개념
코루틴 = 중간에 실행을 멈추고(suspend), 나중에 재개(resume)할 수 있는 함수
일반 함수는 호출 → 완료까지 CPU를 독점하지만, 코루틴은 await / yield 같은 포인트에서 제어권을 반환한다.
언어별 구현
Python (asyncio)
- Green thread (유저 공간 스케줄링)
- 단일 스레드 + 이벤트 루프가 코루틴을 직접 스케줄링
- GIL 제약 그대로 → CPU 병렬성 없음
await포인트에서만 전환 → 개발자가 명시적으로 양보해야 함
async def fetch():
await db.query(...) # 여기서 이벤트 루프에 제어권 반환
Node.js (libuv)
- Python과 구조적으로 동일: 단일 스레드 + 이벤트 루프
- JS 엔진(V8)이 단일 스레드이므로 GIL 같은 개념 자체가 없음 (원래 멀티스레드가 아님)
await/Promise/callback모두 같은 이벤트 루프 위에서 동작- libuv가 I/O를 OS 커널 비동기 API(epoll, kqueue)로 위임
async function fetch() {
await db.query(...) // event loop에 제어권 반환
}
Python과 Node는 사실상 같은 모델이다. 차이는 Python은 GIL이 있고, Node는 원래부터 단일 스레드 설계라는 것.
Go (goroutine)
- M:N 스레딩 — Go 런타임이 M개의 goroutine을 N개의 OS 스레드에 매핑
- GIL 없음 → 멀티코어 CPU를 진정으로 병렬 활용
GOMAXPROCS수만큼 OS 스레드를 띄우고, goroutine을 자동 분배- 개발자가
await없이 그냥go func()쓰면 됨 — 런타임이 알아서 스케줄링
go func() {
db.Query(...) // goroutine이 블로킹되면 런타임이 다른 goroutine으로 교체
}()
핵심 차이 요약
| Python | Node.js | Go | |
|---|---|---|---|
| 스레드 모델 | 단일 스레드 | 단일 스레드 | M:N 멀티스레드 |
| CPU 병렬성 | ❌ (GIL) | ❌ (원래 단일) | ✅ (멀티코어) |
| 스케줄링 주체 | 이벤트 루프 | 이벤트 루프 | Go 런타임 |
| 전환 방식 | 명시적 (await) | 명시적 (await) | 자동 (런타임 선점) |
| 메모리/코루틴 | ~수 KB | ~수 KB | ~2KB (초기) |
Go의 goroutine이 가장 강력한 이유가 여기 있다. Python/Node는 I/O bound에서만 진가를 발휘하고, CPU bound 작업이 끼어들면 전체가 막히지만, Go는 CPU bound도 멀티코어로 병렬 처리한다.
같은 "비동기"라는 단어 뒤에 전혀 다른 철학이 숨어 있다. 언어를 선택할 때, 또는 성능 문제를 디버깅할 때 이 차이를 아는 것과 모르는 것은 다른 결과를 낳는다.