동시성 모델 비교: OS 스레드, 이벤트 루프, 고루틴, 액터 및 Async/Await
동시성 모델의 최종 맵: OS 스레드(Java), 녹색 스레드/고루틴(Go), 단일 스레드 이벤트 루프(Node.js), 행위자 모델(Erlang/Elixir), async/await(Python/Rust). 언제 무엇을 사용해야 하는지, 장단점 및 비교 벤치마크.
경쟁이 어렵기 때문에
동시성은 "진행 중인" 여러 작업을 관리하는 프로그램의 기능입니다. 동시에 — 반드시 병렬일 필요는 없습니다. 그만큼 병행 그것은 처형이다 여러 물리적 코어에서 동시에. 두 개념 사이의 혼동은 많은 문제의 원인입니다. 버그와 잘못된 아키텍처 결정.
2026년에는 5가지 주요 경쟁 모델이 있으며, 각각 서로 다른 장단점이 있습니다. 아니다 절대적으로 "최고의" 모델이 있습니다. 선택은 워크로드 유형에 따라 다릅니다(I/O 바인딩 대 CPU 바인딩), 언어, 생태계 및 대기 시간 요구 사항.
모델 1: OS 스레드(Java, C++)
가장 전통적인 모델: 각 동시성 단위는 실 운영 체제의. 커널은 스케줄링, 컨텍스트 전환 및 통신을 처리합니다. 뮤텍스로 보호된 공유 메모리를 통한 스레드 간.
// Java: thread tradizionale vs virtual thread (Java 21)
// Thread OS tradizionale — costoso: ~1MB stack, scheduling kernel
Thread platformThread = new Thread(() -> {
processRequest(); // blocca il thread OS durante I/O
});
platformThread.start();
// Virtual Thread (Java 21, Project Loom) — leggero: ~2KB stack iniziale
Thread virtualThread = Thread.ofVirtual().start(() -> {
processRequest(); // blocca solo il virtual thread, non il carrier
});
// Un milione di virtual thread sono praticabili
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> handleRequest());
}
} // attende il completamento
장점: 간단한 정신 모델, 자동으로 여러 코어 활용, 성숙한 라이브러리
에 맞서: 고가의 OS 스레드(1MB 이상의 스택, 전환 오버헤드), 공유 메모리 경쟁 조건, 수천 개의 스레드로 제한된 확장
언제: CPU 바인딩된 작업, 스레드 풀이 있는 Java/C++, 혼합 로드
패턴 2: 단일 스레드 이벤트 루프(Node.js, JavaScript)
JavaScript는 단일 스레드입니다. 단 하나의 실행 스레드와 이벤트 루프가 있습니다.
콜백을 관리합니다. 비동기 I/O(네트워크, 파일 시스템)가 운영 체제에 위임됩니다.
통해 libuv 완료된 작업은 콜백 대기열에 배치됩니다.
// Node.js: event loop in azione
// Tutto esegue sullo stesso thread — nessun race condition!
const http = require('http');
http.createServer((req, res) => {
// Questa callback non blocca il thread
fetchUserData(req.userId)
.then(user => {
return fetchOrders(user.id); // altra I/O non bloccante
})
.then(orders => {
res.json({ user, orders });
})
.catch(err => res.status(500).json({ error: err.message }));
}).listen(3000);
// Async/await (zucchero sintattico sopra Promise):
async function handleRequest(req, res) {
const user = await fetchUserData(req.userId); // non blocca il thread
const orders = await fetchOrders(user.id); // non blocca il thread
res.json({ user, orders });
}
장점: 경쟁 조건 없음(단일 스레드), I/O 바인딩을 위한 매우 높은 동시성, 거대한 npm 생태계
에 맞서: CPU 바인딩으로 모든 것을 차단하고 콜백 지옥(async/await로 완화), 진정한 병렬성을 위한 작업자 스레드
언제: 동시 I/O 요청이 많은 API 서버, 실시간 앱, BFF 레이어
모델 3: 고루틴 및 채널(Go)
구현하기 순차 프로세스 통신(CSP): 초경량 고루틴 (2KB 초기 스택, 동적으로 증가)은 형식화된 채널을 통해 전달됩니다. Go 만트라: "기억을 공유하여 소통하지 말고, 소통으로 기억을 공유하라."
// Go: goroutine e channel
package main
import (
"fmt"
"sync"
)
// Fan-out/Fan-in pattern con goroutine
func processItems(items []Item) []Result {
results := make(chan Result, len(items))
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) { // avvia goroutine — ~2KB stack
defer wg.Done()
result := processItem(i) // eseguito concorrentemente
results <- result
}(item)
}
// Chiudi il channel quando tutte le goroutine completano
go func() {
wg.Wait()
close(results)
}()
// Raccoglie i risultati
var collected []Result
for r := range results {
collected = append(collected, r)
}
return collected
}
// Channel per comunicazione sicura tra goroutine
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i // invia sul channel (blocca se pieno)
}
close(ch)
}
func consumer(ch <-chan int) {
for v := range ch { // riceve finché il channel è aperto
fmt.Println(v)
}
}
장점: 초경량 고루틴(수백만 개 실행 가능), 채널로 경합 상태 방지, 런타임으로 스케줄링 관리
에 맞서: CSP 모델에는 학습이 필요하며, 채널이 닫히지 않은 경우 goroutine 누출, Go 1.18 이전에는 제네릭이 없습니다.
언제: 높은 I/O 동시성, 데이터 파이프라인, CLI 도구를 갖춘 백엔드 서비스
패턴 4: 비동기/대기(Python, Rust)
비동기/대기는 협동 경쟁: 작업은 명시적으로 포기합니다.
I/O 대기 지점에서 제어(await). JavaScript 이벤트 루프와 달리
(내장 런타임), Python 및 Rust에는 명시적 런타임(asyncio, Tokyo)이 필요합니다.
// Python: asyncio con TaskGroup (Python 3.11+)
import asyncio
import aiohttp
async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
async with session.get(url) as response:
return await response.text()
async def fetch_all_parallel(urls: list[str]) -> list[str]:
async with aiohttp.ClientSession() as session:
# TaskGroup garantisce che tutti i task completino o vengano cancellati
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch_url(session, url)) for url in urls]
return [task.result() for task in tasks]
# Rust: Tokio async/await (zero-cost)
use tokio::time::{sleep, Duration};
async fn fetch_data(id: u64) -> String {
sleep(Duration::from_millis(100)).await; // simula I/O
format!("data_{}", id)
}
#[tokio::main]
async fn main() {
// Join concorrente senza allocazioni aggiuntive (zero-cost)
let (r1, r2, r3) = tokio::join!(
fetch_data(1),
fetch_data(2),
fetch_data(3),
);
println!("{}, {}, {}", r1, r2, r3);
}
장점: 대기 지점에 대한 명시적인 제어, 공유 스택에 대한 경합 조건 없음, Rust의 비용 없음
에 맞서: "비동기 질병"(비동기를 호출하는 경우 모든 함수는 비동기여야 함), 더 복잡한 디버깅
언제: I/O 바인딩(웹 스크래핑, API 호출)용 Python, 고성능 시스템용 Rust
모델 5: 배우 모델(Erlang/Elixir, Akka)
행위자 모델은 가장 고립되어 있습니다. 각 행위자는 자신만의 비공개 상태를 가지며 통신합니다. 메시지를 통해서만. 공유 메모리도 없고 뮤텍스도 없습니다. 모든 액터는 독립적인 경량 프로세스.
// Elixir: GenServer (actor model)
defmodule Counter do
use GenServer
# Interfaccia pubblica
def start_link(initial \\ 0) do
GenServer.start_link(__MODULE__, initial, name: __MODULE__)
end
def increment() do
GenServer.call(__MODULE__, :increment)
end
def get_count() do
GenServer.call(__MODULE__, :get)
end
# Implementazione (private)
def init(initial), do: {:ok, initial}
def handle_call(:increment, _from, count) do
{:reply, count + 1, count + 1} # reply, valore_risposta, nuovo_stato
end
def handle_call(:get, _from, count) do
{:reply, count, count}
end
end
# La BEAM VM può avere milioni di processi leggeri
# con supervisione automatica (OTP supervisor tree)
장점: 전체 격리(액터 충돌이 전파되지 않음), 설계에 따른 내결함성, 기본적으로 분산됨
에 맞서: 메시지 직렬화 오버헤드, 행위자가 많은 디버깅 시스템이 복잡함
언제: 수백만 개의 연결을 갖춘 내결함성 분산 시스템, 통신, 실시간 게임, IoT
비교 벤치마크
경쟁: 모델 선택 안내
- 많은 I/O 요청(웹 API 서버): goroutine 또는 Node.js 이벤트 루프로 이동 - 둘 다 수만 개의 동시 실행으로 확장됩니다.
- CPU 바인딩(ML 추론, 인코딩): 작업자 풀이 있는 Java 또는 Go OS 스레드 — 모든 코어 사용
- 매우 낮은 대기 시간(거래, 게임): Rust Tokyo 또는 Go — 런타임에 오버헤드가 없음
- 분산 내결함성(통신사, IoT): Elixir/Erlang Actor 모델 — 기본 감독 트리
- 데이터 과학, 스크립팅: I/O를 위한 Python asyncio, CPU 바인딩을 위한 다중 처리
- 자바 엔터프라이즈 팀: Java 21 가상 스레드 — 클래식 스레드와 동일한 정신 모델, 고루틴처럼 확장
결론
보편적으로 우수한 경쟁 모델은 없습니다. 올바른 선택은 다음에 달려 있습니다. 워크로드(I/O 대 CPU), 팀별(기술 및 선호도), 생태계별(사용 가능한 라이브러리) 비기능적 요구 사항(대기 시간, 처리량, 내결함성).
시리즈의 다음 기사에서는 각 모델을 자세히 살펴보겠습니다. Node.js에서 가장 오해받는 구성요소인 JavaScript 이벤트 루프.







