Go Concurrency: Goroutines, Channels, Select pattern

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.

Sơ đồ Go concurrency minh họa select, WaitGroup, worker pool

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

Minh họa select statement với timeout và default case

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

Sơ đồ worker pool: job queue, worker goroutines, result channel

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ùng defer mu.Unlock() đảm bảo giải phóng. sync.RWMutex cho 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

Tôi là một lập trình viên IOS. Code chính là IOS nhưng thỉnnh thoảng vẫn đá sang Android hoặc web. Mặc dù không quá thông thạo nhưng tôi sẽ chia sẻ những kiến thức mà mình đã tìm hiểu, áp dụng qua.

Bài viết liên quan

Bevy Engine: Xây dựng game 2D/3D với Rust hiệu năng cao

Bevy là game engine mã nguồn mở được xây dựng bằng Rust, tập trung vào khả năng mở rộng (data-driven) và hiệu năng. Với kiến trúc ECS (Entity Component System)…

Xem thêm

SQLite FTS5 Full-Text Search: Tìm kiếm nội dung trong app không cần server

SQLite FTS5 (Full-Text Search version 5) là công cụ tìm kiếm toàn văn bản tích hợp sẵn trong SQLite, cho phép tìm kiếm nội dung văn bản nhanh chóng mà…

Xem thêm

Golang Concurrency Patterns: Worker Pool, Pipeline, Fan-out/Fan-in thực tế

Golang nổi tiếng với mô hình concurrency “share memory by communicating” thay vì “share memory by locking”. Nhưng pattern thông thường như worker pool, pipeline, fan-out/fan-in không tự động xuất…

Xem thêm
0 0 đánh giá
Article Rating
Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất