Bài coroutine nói unit có thể suspend / resume. Còn thiếu một câu: ai là người bấm resume?
Trên Node, browser, asyncio (và họ hàng): hầu hết lúc đó là
event loop — một vòng lặp trên
một thread (JS main thread / loop thread) quyết định
“tiếp theo chạy đoạn code nào”. Không phải OS tự cắt
async function như cắt thread.
Coroutine / async task = việc dang dở (có state)
Event loop = điều phối viên: khi nào chạy tiếp việc nào
OS thread = chỗ loop đang ngồi (thường 1 cho JS)
Demo (1 loop, 3 handler — await vs sync block): event loop · resume vs kẹt.
Hình dung: 1 bếp trưởng, nhiều phiếu order
Loop ≈ một đầu bếp. Mỗi request/handler là phiếu:
- Đang làm (ôm bếp) — chỉ một phiếu tại một thời điểm trên thread đó.
- Chờ lò / chờ shipper (I/O) — phiếu ghim “khi chuông reo thì làm tiếp”, bếp đi phiếu khác.
- Chuông reo — không nhảy vào giữa nhát dao; xếp hàng đợi, bếp xong việc hiện tại (và hết microtask) mới lấy.
Đó là dealing with trên 1 core: nhiều phiếu mở, không phải nhiều đầu bếp (doing parallel).
Ba chỗ việc sống
1) Call stack — đang chạy đồng bộ (ôm loop)
2) Hàng đợi task — timer, I/O xong, message… (macrotask / phase)
3) Microtask — Promise reaction, queueMicrotask (JS)
· asyncio: ready callbacks tương tự “chạy hết trước khi sleep”
Mô hình tinh thần (JS; Node có thêm phase libuv nhưng ý giống):
while (còn việc) {
// A. Lấy 1 macrotask (timer / I/O callback / …)
chạy đồng bộ đến hết stack
// B. Drain microtask: trong khi còn microtask
// chạy hết — kể cả microtask mới sinh ra lúc này
// → trước khi macrotask / render tiếp theo
// C. (browser) có thể render; (Node) phase khác: check, close…
}
Hệ quả hay quên:
microtask có thể “đói” macrotask nếu bạn liên tục
queueMicrotask / chain Promise không nhường. Timer 0ms
không chạy xen khi microtask còn đầy.
await làm gì với loop?
async function handle() {
const a = 1 + 1 // ① stack, ôm loop
const row = await db.get() // ②
return row.id // ③
}
- ① Chạy sync trên stack — không ai chen ngang (cooperative).
-
②
awaitmột Promise chưa settle:- function suspend (continuation = “chạy từ dòng sau await”),
- nhả stack / nhả loop — loop đi macrotask hoặc microtask khác,
-
khi
db.getxong: runtime xếp continuation (thường qua microtask khi Promise fulfill) — không cướp loop giữa chừng đoạn sync khác.
- ③ Chỉ chạy sau khi loop chọn resume continuation đó.
Vậy “ai resume sau await?” = event loop (sau khi Promise settle + đến lượt queue), không phải thread pool tự động (trừ khi library offload bên dưới).
Nối non-blocking: await chỉ “non-block loop” nếu lúc chờ, loop thật sự rảnh để deal việc khác — tức I/O không ôm thread JS.
Ai bị kẹt khi sync?
Mọi thứ trên cùng loop serialize trên 1 stack. Trong lúc bạn:
// 1) CPU nặng trên main
for (let i = 0; i < 1e9; i++) crunch(i)
// 2) I/O sync
fs.readFileSync('big') // Node: block thread loop
// 3) JSON.parse multi-MB, crypto sync, regex thảm họa
thì:
- Không chạy handler khác.
- Không resume await đã ready (data về rồi vẫn xếp hàng).
- Timer trễ; WebSocket/heartbeat trễ; health check chết.
- “Concurrent trên giấy” — serial trên thực tế. Giống blocking cả deal.
Demo cột phải cố ý ôm loop vài tick: các resume khác đứng chờ dù I/O đã xong.
Microtask vs macrotask — ví dụ một dòng
setTimeout(() => console.log('timer'), 0)
Promise.resolve().then(() => console.log('micro'))
console.log('sync')
// In: sync → micro → timer
// Vì: hết stack → drain microtask → mới macrotask timer
await Promise đã resolved vẫn
lịch continuation microtask (nhường một nhịp) — không chạy
ngay như hàm sync. Đó là chỗ người ta nhầm “await luôn tiếp liền
không qua loop”.
Node: libuv, không chỉ “một queue”
Node loop (đơn giản hóa): timers → pending → idle/prepare → poll
(I/O) → check → close; giữa các phase vẫn đụng microtask/nextTick.
fs.readFile non-sync thường worker pool libuv; callback
“I/O xong” vào phase phù hợp rồi mới chạy JS.
Bạn hiếm khi cần thuộc hết phase. Cần thuộc: JS của bạn vẫn một thread; block thread đó = block mọi callback/async continuation.
Offload — khi việc không được ôm loop
-
CPU:
worker_threads, process pool, job queue riêng — xem hướng thread pool / offload và parallel doing. -
Đừng “fake async”:
await Promise.resolve(heavySync())vẫn chạy heavy trước khi await nhường — loop vẫn kẹt đúng đoạn sync. - Đếm việc bay: in-flight inventory — loop delay / event loop lag là metric proxy “nợ” trên 1 thread.
Checklist debug “async mà chậm / đơ”
- CPU một core 100%, event loop delay tăng? → sync CPU trên loop.
- Có
*Sync, parse lớn, crypto sync trong request path? - Promise/microtask loop vô hạn? (hiếm hơn block CPU nhưng đói timer)
- Dependency chậm nhưng loop rảnh? → không phải kẹt loop; là I/O / pool — connection pool, timeout budget.
- Cần hủy khi client cút? Signal xuống I/O — cancellation — loop resume path lỗi abort, đừng để query mồ côi.
Đọc tiếp
- Coroutine — unit suspend/resume; loop là người schedule resume.
- Blocking vs non-blocking — chờ có nhả unit không.
- Concurrency vs parallel — deal trên 1 loop ≠ doing nhiều core.
Một câu để nhớ
Event loop = điều phối viên resume trên một thread.
await xếp “làm tiếp sau”; sync ôm loop = không ai được resume.
Async/await không tạo parallel. Nó chỉ cho phép nhiều việc dang dở trong lúc một đoạn sync chạy. Giữ đoạn đó ngắn — hoặc offload — là giữ concurrency sống.