
Go concurrency là một trong những điểm mạnh nhất của ngôn ngữ Go, cho phép xử lý hàng ngàn tác vụ đồng thời nhờ goroutine nhẹ (chỉ ~2KB stack) và channel truyền thông an toàn. Bài viết này giải thích cơ chế scheduler M:N, pattern worker pool, fan-out/fan-in, và cách tránh goroutine leak.

Khác với thread truyền thống (OS thread ~1-8MB stack), goroutine được quản lý bởi Go runtime scheduler theo mô hình M:N: M OS thread chạy N goroutine. Scheduler dùng work-stealing queue, preemptive scheduling (từ Go 1.14) để đảm bảo công bằng. Điều này cho phép một chương trình Go tạo hàng triệu goroutine mà không cạn kiệt tài nguyên.
Goroutine: Khái niệm và khởi tạo
Goroutine là hàm chạy độc lập, khởi tạo bằng từ khóa go:
go func() {
fmt.Println("Chạy song song")
}()
// main tiếp tục ngay lập tức, không chờ goroutine kết thúc
Mỗi goroutine có stack riêng, có thể grow/shrink động. Runtime cấp phát stack nhỏ ban đầu (2KB) và mở rộng khi cần. Điều này hiệu quả hơn thread OS rất nhiều.
Channel: Truyền thông an toàn giữa goroutine
Channel là hàng đợi FIFO có kiểu (chan T). Hai loại chính:
- Unbuffered channel (
make(chan int)): Gửi/nhận đồng bộ (rendezvous). Sender block cho đến khi receiver sẵn sàng và ngược lại. Đảm bảo handoff an toàn. - Buffered channel (
make(chan int, 10)): Gửi không block cho đến khi đầy buffer; nhận không block cho đến khi rỗng. Dùng để decouple tốc độ producer/consumer.
Channel directional type: chan<- int (send-only), <-chan int (receive-only) — enforce tại compile time.
Select: Đa kênh không block

select cho phép goroutine chờ trên nhiều channel cùng lúc, chọn ngẫu nhiên case ready để tránh starvation:
select {
case msg := <-ch1:
// xử lý msg
case ch2 <- val:
// gửi val
case <-time.After(5 * time.Second):
// timeout sau 5s
default:
// non-blocking fallback
}
Pattern phổ biến: time.After cho timeout, default cho non-blocking poll.
sync.WaitGroup: Đồng bộ hóa nhóm goroutine
sync.WaitGroup là counter nguyên tử:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// công việc
}(i)
}
wg.Wait() // block cho đến khi counter = 0
Lưu ý: Luôn truyền con trỏ (*sync.WaitGroup) cho hàm/closure, không copy value (WaitGroup chứa no-copy checker).
Pattern Worker Pool: Kiểm soát độ song song

Worker pool giới hạn số goroutine chạy cùng lúc, tránh quá tải CPU/memory:
jobs := make(chan Job, 100)
results := make(chan Result, 100)
// Khởi động 4 worker
for w := 1; w <= 4; w++ {
go func(id int) {
for j := range jobs {
results <- process(j)
}
}(w)
}
// Gửi job
for j := range inputData {
jobs <- j
}
close(jobs) // báo hiệu không còn job mới
// Thu kết quả
for i := 0; i < len(inputData); i++ {
<-results
}
Số worker thường bằng runtime.NumCPU() hoặc config tùy workload.
Fan-out / Fan-in: Phân tán và tổng hợp
Fan-out: một input chia cho nhiều worker xử lý song song. Fan-in: nhiều output gom về một channel duy nhất. Kết hợp tạo pipeline xử lý dữ liệu lớn hiệu quả.
// Fan-out
for _, url := range urls {
go func(u string) { results <- fetch(u) }(url)
}
// Fan-in
for range urls {
<-results
}
Context: Hủy bỏ và deadline
Package context cung cấp WithCancel, WithTimeout, WithDeadline để lan tín hiệu hủy qua call stack:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go func() {
select {
case <-ctx.Done():
return // bị hủy
case result := <-doWork():
// xử lý
}
}()
Cạm bẫy thường gặp
| Vấn đề | Nguyên nhân | Giải pháp |
|---|---|---|
| Goroutine leak | Channel send block vô tận (không receiver) | Dùng buffered channel, context timeout |
| Race condition | Shared memory không đồng bộ | Dùng channel, sync.Mutex, atomic |
| Panic: close closed channel | Close channel đã close | Chỉ close ở sender, dùng sync.Once |
| Nil channel block | Send/recv trên nil channel | Khởi tạo channel trước khi dùng |
Advanced sync primitives: Mutex, Once, Cond
Bên cạnh channel, package sync cung cấp các primitive cấp thấp hơn cho kiểm soát truy cập bộ nhớ chia sẻ:
- sync.Mutex: Khóa loại trừ tương hỗ.
Lock()/Unlock(). Dùngdefer mu.Unlock()đảm bảo giải phóng.sync.RWMutexcho phép nhiều reader đồng thời (RLock), writer độc quyền (Lock). - sync.Once: Đảm bảo hàm chỉ chạy một lần, thread-safe. Phù hợp cho lazy initialization (singleton, config load).
var once sync.Once; once.Do(initFunc). - sync.Cond: Condition variable trên Mutex.
Wait()giải phóng khóa và block;Signal()đánh thức 1 waiter,Broadcast()đánh thức tất cả. Dùng cho producer-consumer phức tạp hơn channel.
Khi nào dùng Mutex thay channel? Khi critical section ngắn, hiệu năng quan trọng, hoặc logic phức tạp không phù hợp message passing. Quy tắc: Don’t communicate by sharing memory, share memory by communicating — ưu tiên channel, dùng Mutex khi cần thiết.
Kết luận
Go concurrency model (goroutine + channel + select) là nền tảng cho hệ thống hiệu năng cao. Hiểu rõ scheduler M:N, pattern worker pool, fan-out/fan-in, và context cancellation giúp viết code đồng thời đúng, an toàn, dễ bảo trì. Luôn profile với go tool pprof và race detector (go run -race) trước khi deploy production.
Tham khảo thêm: Go Wiki: Learn Concurrency, Go Blog: Concurrency is not Parallelism, Dev.to: Channels, Select và Worker Pool
