Event Loop vs. Threads vs. the Actor Model TODO
Concept
The three models differ in what unit they slice concurrency into and how they share state. An event loop runs a single thread that non-blockingly cycles through ready I/O events and runs callbacks/tasks, so there's almost no context switching or locking — but one CPU-bound task blocking the loop stalls everything. The thread model lets the OS do preemptive scheduling, so you can write blocking code as-is and naturally use multiple cores, but shared memory has to be guarded with locks, which brings contention, deadlocks, and false sharing. The actor model confines state inside each actor and only allows communication through asynchronous messages, eliminating shared memory itself — the tradeoffs shift to message-copy cost, mailbox backpressure, and how much ordering is guaranteed. Real-world runtimes are usually hybrids: Go's goroutines, for instance, use a user-level scheduler that multiplexes many lightweight tasks onto a small number of OS threads, with an event-based I/O poller underneath.
A large share of concurrency bugs and latency spikes come from violating the assumptions behind whichever model was chosen — don't block the loop, guard shared state with locks, mailboxes aren't unbounded.
Code & Formula
# 이벤트 루프 vs 스레드 vs 액터 모델 — 상태를 액터 안에 가두고 메시지 큐로만 통신하는 최소 액터를 구현한다.
# 공유 메모리가 없으니 락도 필요 없고, 순서 보장은 "한 액터는 메일박스 메시지를 하나씩만 처리한다"에서 나온다.
import threading
import queue
class Actor:
def __init__(self, name, handle_fn):
self.name = name
self.mailbox = queue.Queue() # 오직 메시지로만 상태에 접근 — 공유 메모리 자체가 없다
self._handle_fn = handle_fn
self._state = {}
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
def send(self, msg):
self.mailbox.put(msg) # 비동기 전송 — 보내는 쪽은 블록되지 않는다
def _run(self):
while True:
msg = self.mailbox.get() # 메시지를 하나씩만 순차 처리 -> 액터 내부 상태는 절대 경합하지 않음
if msg is None: # 종료 신호
break
self._handle_fn(self._state, msg)
def stop_and_join(self):
self.send(None)
self._thread.join()
def counter_handler(state, msg):
op, payload = msg
if op == "incr":
state["value"] = state.get("value", 0) + payload
elif op == "get":
reply_queue = payload
reply_queue.put(state.get("value", 0)) # 결과는 회신용 큐로 되돌려줌 — 여기도 메시지 전달일 뿐
account = Actor("counter", counter_handler)
# 여러 "클라이언트" 가 동시에 메시지를 보내도, 액터 내부 값은 큐를 통해 직렬화되어 안전하다.
def client(n):
for _ in range(100):
account.send(("incr", n))
clients = [threading.Thread(target=client, args=(i,)) for i in (1, 2, 3, 4)]
for t in clients:
t.start()
for t in clients:
t.join()
reply_queue = queue.Queue()
account.send(("get", reply_queue))
total = reply_queue.get(timeout=5) # get 메시지가 처리되어 회신이 올 때까지 블로킹 대기(결정론적 동기화)
account.stop_and_join()
expected = (1 + 2 + 3 + 4) * 100
print("expected total:", expected)
print("actor-computed total:", total)
print("no locks used, no data race possible:", total == expected)
docs/code/algorithms/algorithms-42.py
Exercise
Build the same HTTP echo server in Node.js (event loop) and Go (goroutines), put a several-hundred-millisecond CPU computation into the handler, and measure with a load tool how p99 latency changes.
Practical Connection
A blockchain indexer or an order book matching engine typically confines matching state to a single actor/single thread and only runs I/O asynchronously, which gets you ordering guarantees and throughput at the same time.
Where it lands in Jayverse
- Verex: confirm the CLOB matching engine runs as a single actor or thread and audit any code path that could block it with a CPU-bound task. That would stall all matching for every market.
- Devnet: choose each Cloud Run service's concurrency model from this framework. An event loop for I/O-bound API routes, a single-actor design for stateful matching or settlement logic.
- Rabbit: isolate session-key and mandate execution that touches shared state, such as nonces or allowances, per actor rather than guarding it with ad hoc locks. This avoids the contention and deadlock failure mode named here.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| slice (concurrency) into | ~단위로 쪼개다, 나누다 · 동시성 모델이 작업을 어떤 단위로 분할하는지 말할 때. "what unit they slice concurrency into" |
| stall everything | 전체를 멎게 하다 · 하나의 작업이 시스템 전체를 멈춰 세울 때. "blocking the loop stalls everything" |
| guard (shared state) | (공유 상태를) 보호하다, 지키다 · 락 등으로 동시 접근을 막을 때. "shared memory has to be guarded with locks" |
| confine | 가두다, 한정하다 · 상태를 한 영역 안에만 머물게 제한할 때. "confines state inside each actor" |
| backpressure | 백프레셔(역압), 처리 속도에 맞춘 유입 제어 · 큐가 넘치지 않도록 압박을 되돌려주는 메커니즘. "mailbox backpressure" |
| multiplex | 다중화하다 · 적은 자원(스레드)에 많은 작업을 겹쳐 배정할 때. "multiplexes many lightweight tasks onto a small number" |
| violate (an assumption) | 전제를 어기다, 깨뜨리다 · 모델이 기대는 규칙을 지키지 않아 문제가 생길 때. "violating the assumptions behind whichever model was chosen" |
| false sharing | 거짓 공유(false sharing) · 서로 다른 변수가 같은 캐시라인에 걸쳐 있어, 실제론 안 겹치는 데이터인데도 캐시 무효화가 반복되며 성능이 떨어지는 현상. "which brings contention, deadlocks, and false sharing" |
| p99 | p99 지연시간 · 요청의 99번째 백분위수 지연시간, 평균이 아니라 꼬리(tail) 구간의 최악 지연을 보는 지표. "measure with a load tool how p99 latency changes" |
| goroutine | 고루틴(goroutine) · Go 언어의 경량 사용자 레벨 태스크, 소수의 OS 스레드 위에 다중화되어 실행됨. "Go's goroutines, for instance, use a user-level scheduler" |
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.