L27 — Goroutine và Channel

Ba module tương tác minh hoạ ba "khái niệm sống" của concurrency trong Go: (1) Go scheduler nuôi hàng vạn goroutine trên vài OS thread, (2) channel làm khớp nối giữa goroutine, (3) worker pool — pattern phổ thông kết hợp cả hai.

Module 1 — Go scheduler (M:N)

Mỗi OS thread (P/M) là một "công nhân" CPU thật. Go runtime nuôi một hàng đợi run queue cho mỗi thread, và đẩy goroutine (G, ~2 KB stack) lên đó. Khi 1 thread hết việc, nó steal goroutine từ thread khác → tận dụng tối đa CPU.

G chờ chạy G đang chạy G xong G bị steal
pending: 0 running: 0 done: 0 steals: 0
✓ Completed
Vì sao Go scale tốt? Goroutine stack ≈ 2 KB (growable), OS thread stack ≈ 1–8 MB. 10 000 goroutine ≈ 20 MB RAM; 10 000 OS thread ≈ ~80 GB RAM (∞).
Khi N nhỏ (vài thread) + M rất lớn (vạn goroutine), mô hình M:N "ép" mọi goroutine xếp hàng trên vài run queue, switch trong user-space (~100 ns) thay vì kernel call (~1 µs).

Module 2 — Channel handoff

Channel = ống dẫn dữ liệu giữa hai goroutine. Unbuffered: gửi/nhận phải gặp nhau (rendezvous) → một bên đi trước sẽ block. Buffered (cap N): có "kho" N slot, sender chỉ block khi đầy, receiver chỉ block khi rỗng.

📤 Sender
idle
chan int (unbuf)
📥 Receiver
idle
Thử 3 kịch bản: (1) Unbuffered + Send trước Receive → sender block; (2) Buffered cap 3 + Send 4 lần liên tiếp → lần 4 block; (3) Close rồi Receive → trả về zero-value (0) + ok = false.

Module 3 — Worker pool

Pattern kinh điển: 1 channel jobs, N worker goroutine cùng range trên jobs → Go runtime tự phân phối (load balance). Tăng N → throughput tăng cho tới khi chạm trần CPU (hoặc bottleneck I/O).

pending: 0 in-progress: 0 done: 0 throughput: 0 jobs/s

Jobs queue (chan)

Workers

Completed

Quan sát: với 3 worker, 10 job × 900 ms ≈ ~3 s wall-clock (vs 9 s nếu chạy tuần tự). Tăng worker lên 8 → ~1.5 s. Nhưng nếu tăng tiếp lên 20 trong khi máy chỉ có 4 core → throughput không tăng — hết CPU.