Go Zero to Hero - Bài 54: Unbuffered Channel vs Buffered Channel
Trong hệ thống xử lý đồng thời (concurrency), việc lựa chọn đúng loại Channel đóng vai trò quyết định đến hiệu năng và tính ổn định của hệ thống. Golang cung cấp hai biến thể chính: Unbuffered Channel và Buffered Channel.
1. Nhắc lại: Unbuffered Channel (Giao dịch "Trao tay")
Đây là loại Channel mặc định. Nó có sức chứa bằng 0.
- Cú pháp:
make(chan int)hoặcmake(chan int, 0) - Bản chất: Giao tiếp đồng bộ (Synchronous).
- Cơ chế hoạt động: Giống như một cuộc giao dịch "tiền trao cháo múc":
- Người gửi nhét dữ liệu vào ống -> Bị Block ngay lập tức -> Đứng chờ cho đến khi có người nhận rút dữ liệu đó ra.
- Người nhận thò tay vào ống rỗng -> Bị Block ngay lập tức -> Đứng chờ cho đến khi có người gửi nhét dữ liệu vào.
- Ứng dụng: Tuyệt vời để Đồng bộ hóa (Synchronization). Nó đảm bảo chắc chắn 100% rằng một sự kiện đã được phía bên kia tiếp nhận thì luồng hiện tại mới được chạy tiếp.
2. Buffered Channel (Giao dịch qua "Thùng chứa")
Buffered Channel là ống nước có gắn thêm một cái thùng chứa (Buffer) ở giữa, với sức chứa do bạn định đoạt.
- Cú pháp:
make(chan int, capacity)(ví dụ:make(chan int, 3)tạo ra ống chứa được 3 phần tử). - Bản chất: Giao tiếp bất đồng bộ (Asynchronous) trong giới hạn cho phép.
- Cơ chế hoạt động:
- Người gửi CHỈ bị Block khi THÙNG ĐÃ ĐẦY. Nếu thùng còn chỗ, người gửi cứ việc ném dữ liệu vào đó rồi lập tức quay đi làm việc khác, không cần quan tâm đã có ai nhận hay chưa.
- Người nhận CHỈ bị Block khi THÙNG RỖNG. Nếu trong thùng có sẵn dữ liệu, người nhận cứ việc lấy ra xài mà không cần chờ người gửi.
3. Thực chiến: Giải cứu giờ cao điểm tại cổng AFC
Hãy xem Buffered Channel hoạt động như một cái "hàng đợi" (Queue) giúp hệ thống không bị nghẽn mạng:
package main
import (
"fmt"
"time"
)
func main() {
// 1. Tạo Buffered Channel với sức chứa 3 chiếc vé
// Điều này giống như việc bố trí một cái khay chứa tạm thời ở trạm
ticketQueue := make(chan string, 3)
// 2. Kích hoạt luồng Server (Người nhận) chạy ngầm
go func() {
for {
// Server xử lý hơi chậm, tốn 2 giây để lưu 1 vé vào DB
ticket := <-ticketQueue
fmt.Println("[Server] 💾 Đã lưu vé vào DB:", ticket)
time.Sleep(2 * time.Second)
}
}()
// 3. Luồng Main đóng vai trò Máy quét thẻ (Người gửi)
fmt.Println("[Scanner] Bắt đầu giờ cao điểm...")
// 3 khách hàng quẹt thẻ liên tục
ticketQueue <- "VÉ_01"
fmt.Println("[Scanner] Đã đẩy VÉ_01 vào hàng đợi")
ticketQueue <- "VÉ_02"
fmt.Println("[Scanner] Đã đẩy VÉ_02 vào hàng đợi")
ticketQueue <- "VÉ_03"
fmt.Println("[Scanner] Đã đẩy VÉ_03 vào hàng đợi")
// Cả 3 vé được đẩy vào ống KHÔNG BỊ BLOCK, máy quét hoạt động cực mượt!
fmt.Println("[Scanner] Khay đã đầy 3/3. Nếu có người thứ 4 quẹt, máy quét sẽ bị khựng!")
// Dừng luồng main lại một chút để xem Server xử lý nốt
time.Sleep(7 * time.Second)
fmt.Println("--- KẾT THÚC GIỜ CAO ĐIỂM ---")
}
Nếu chạy đoạn code trên, bạn sẽ thấy Máy quét lập tức in ra 3 dòng xác nhận đẩy vé thành công vào hàng đợi mà không hề bị khựng lại chờ Server. Sau đó, Server cứ túc tắc 2 giây lôi ra một vé để xử lý. Đó chính là sức mạnh của việc chia tách tốc độ (Decoupling) bằng Buffered Channel.
4. Bẫy tử thần: Tràn thùng chứa (Deadlock)
Tương tự như Unbuffered Channel, sự bế tắc (Deadlock) vẫn có thể xảy ra với Buffered Channel nếu bạn quá tự tin vào thùng chứa của mình.
Điều gì xảy ra nếu bạn tạo sức chứa là 2, nhưng lại cố tình nhét vào đó 3 phần tử khi không có ai nhận?
func main() {
// Thùng chứa tối đa 2 món
ch := make(chan string, 2)
ch <- "Món 1" // Chạy qua mượt mà (Thùng: 1/2)
ch <- "Món 2" // Chạy qua mượt mà (Thùng: 2/2) - THÙNG ĐÃ ĐẦY!
// 🚨 KÍCH HOẠT PANIC: DEADLOCK
// Lệnh gửi thứ 3 sẽ bị Block vì thùng đã đầy,
// hàm main() đứng khựng lại và không bao giờ thoát ra được.
// ch <- 3
_ = ch
}
💡 Cách phòng tránh: Trong kiến trúc Backend thực tế, không bao giờ có chuyện gửi và nhận dữ liệu trong cùng một Goroutine. Luôn có một Worker Pool (nhóm các Goroutine) chuyên chạy ngầm để liên tục lấy dữ liệu ra khỏi Channel, giữ cho thùng chứa luôn được giải phóng.
5. Tổng kết: Chọn vũ khí nào?
Để không bao giờ thiết kế sai kiến trúc luồng dữ liệu, hãy bám sát bảng tiêu chí này:
| Tiêu chí | Unbuffered Channel (make(chan T)) |
Buffered Channel (make(chan T, size)) |
|---|---|---|
| Sức chứa | Bằng 0 | Lớn hơn 0 |
| Tính chất | Đồng bộ (Sync) | Bất đồng bộ (Async) - Có giới hạn |
| Người gửi Block khi | Luôn luôn (chờ người nhận rút ra) | Chỉ khi thùng rỗng không còn chỗ chứa |
| Người nhận Block khi | Ống không có gì | Thùng hoàn toàn trống rỗng |
| Ứng dụng thực chiến | Đảm bảo tính chắc chắn. Luồng A báo hiệu cho luồng B biết nó đã hoàn thành một công đoạn. | Làm Message Queue nội bộ, cân bằng tải giữa luồng tạo dữ liệu (nhanh) và luồng xử lý (chậm). Giới hạn số lượng truy cập đồng thời (Rate Limiting). |
All rights reserved