👨🍳 Nhiều Đầu Bếp Song Song - Goroutines
Chào mừng đến với đặc sản của Bếp Á! Hôm nay chúng ta học cách vận hành một bếp với nhiều đầu bếp làm việc song song - chính là Goroutines!
🎯 Món Ăn Hôm Nay
Tưởng tượng một nhà hàng phở đông khách vào giờ cao điểm:
- Bàn 1: Gọi phở bò
- Bàn 2: Gọi bún chả
- Bàn 3: Gọi gỏi cuốn
- Bàn 4: Gọi phở gà
- Bàn 5: Gọi nem rán
Nếu chỉ có 1 đầu bếp, khách sẽ chờ rất lâu! Nhưng với Goroutines (nhiều đầu bếp), mọi món được nấu song song!
🥘 Goroutine Là Gì?
Goroutine = Một đầu bếp độc lập trong bếp
Đặc điểm:
- 🏃 Nhanh: Tạo goroutine nhanh như gọi một đầu bếp vào bếp
- 💪 Nhẹ: Mỗi goroutine chỉ tốn ~2KB memory (so với thread ~1MB)
- 🔄 Song song: Hàng ngàn goroutines có thể chạy cùng lúc
- 🎯 Tự động: Go runtime tự động điều phối
🍜 Ẩn Dụ Nhà Hàng:
- 1 goroutine = 1 đầu bếp
- Go runtime = Bếp trưởng điều phối
- Main goroutine = Đầu bếp chính
👨🍳 Câu Chuyện Trong Bếp
Sáng nay nhà hàng nhận được 5 đơn hàng cùng lúc:
Cách 1: Một đầu bếp (Không dùng Goroutine)
func main() {
nauPho() // 10 phút
nauBunCha() // 8 phút
gopGoiCuon() // 5 phút
// Tổng: 23 phút - Khách chờ lâu quá!
}
Cách 2: Nhiều đầu bếp (Dùng Goroutine)
func main() {
go nauPho() // Đầu bếp A nấu phở
go nauBunCha() // Đầu bếp B nấu bún chả
go gopGoiCuon() // Đầu bếp C gọp gỏi cuốn
time.Sleep(12 * time.Second) // Chờ món lâu nhất
// Tổng: 12 phút - Nhanh hơn gần gấp đôi!
}
🎉 Kết quả: Từ khóa
go= Gọi thêm một đầu bếp vào bếp!
📝 Công Thức Nấu (Code Examples)
Ví Dụ 1: Gọi Một Đầu Bếp
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("🏪 Nhà hàng mở cửa!")
// Gọi đầu bếp phụ nấu phở
go nauPho()
// Đầu bếp chính làm việc khác
fmt.Println("👨🍳 Đầu bếp chính chuẩn bị gia vị...")
time.Sleep(3 * time.Second) // Đợi món phở xong
fmt.Println("✅ Ca làm việc kết thúc!")
}
func nauPho() {
fmt.Println("🍜 Bắt đầu nấu phở...")
time.Sleep(2 * time.Second)
fmt.Println("🍜 Phở đã sẵn sàng!")
}
Ví Dụ 2: Nhiều Đầu Bếp Nấu Nhiều Món
func main() {
fmt.Println("🏪 Giờ cao điểm - 5 bàn gọi món!")
// Gọi 5 đầu bếp, mỗi người nấu một món
for soBan := 1; soBan <= 5; soBan++ {
go nauMonChoBan(soBan)
}
time.Sleep(5 * time.Second)
fmt.Println("✅ Tất cả bàn đã được phục vụ!")
}
func nauMonChoBan(soBan int) {
fmt.Printf("👨🍳 Đầu bếp %d: Bắt đầu nấu cho bàn %d\n", soBan, soBan)
time.Sleep(2 * time.Second)
fmt.Printf("✅ Bàn %d: Món ăn sẵn sàng!\n", soBan)
}
Ví Dụ 3: Goroutine Với Hàm Ẩn Danh
func main() {
tenMon := "Phở Bò Đặc Biệt"
// Tạo đầu bếp tạm thời (anonymous function)
go func(mon string) {
fmt.Printf("👨🍳 Đang nấu %s...\n", mon)
time.Sleep(2 * time.Second)
fmt.Printf("✅ %s đã xong!\n", mon)
}(tenMon)
time.Sleep(3 * time.Second)
}
🔍 Giải Thích:
go nauPho()= Gọi đầu bếp phụ nấu phởgo func() {...}()= Thuê đầu bếp tạm thời nấu món cụ thểtime.Sleep()= Chờ đầu bếp hoàn thành (tạm thời, chưa tối ưu)
🔄 WaitGroup - Điểm Danh Đầu Bếp
Vấn đề với time.Sleep():
- Không biết đầu bếp nào xong, đầu bếp nào chưa
- Chờ ước lượng → lãng phí thời gian hoặc thiếu thời gian
Giải pháp: WaitGroup = Bảng điểm danh
Ví Dụ 1: Điểm Danh Cơ Bản
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup // Bảng điểm danh
soMonCanNau := 5
fmt.Println("📋 Điểm danh: Cần " + fmt.Sprint(soMonCanNau) + " đầu bếp")
for i := 1; i <= soMonCanNau; i++ {
wg.Add(1) // Ghi tên đầu bếp vào bảng
go func(id int) {
defer wg.Done() // Đánh dấu hoàn thành khi xong
fmt.Printf("👨🍳 Đầu bếp %d: Bắt đầu nấu\n", id)
time.Sleep(2 * time.Second)
fmt.Printf("✅ Đầu bếp %d: Hoàn thành!\n", id)
}(i)
}
wg.Wait() // Chờ TẤT CẢ đầu bếp hoàn thành
fmt.Println("🎉 Tất cả đầu bếp đã xong việc!")
}
🔍 Giải Thích:
wg.Add(1): Thêm tên đầu bếp vào bảng điểm danhdefer wg.Done(): Đánh dấu "đã xong" khi món nấu xongwg.Wait(): Chờ đến khi TẤT CẢ đều đánh dấu xong
Ví Dụ 2: Nấu Menu Phức Tạp
func nấuMenu(danhSachMon []string) {
var wg sync.WaitGroup
for _, tenMon := range danhSachMon {
wg.Add(1)
go func(mon string) {
defer wg.Done()
nauMon(mon)
}(tenMon)
}
wg.Wait()
fmt.Println("🍽️ Tất cả món đã sẵn sàng phục vụ!")
}
func nauMon(ten string) {
fmt.Printf("👨🍳 Đang nấu %s...\n", ten)
time.Sleep(2 * time.Second)
fmt.Printf("✅ %s đã xong!\n", ten)
}
🔒 Mutex - Khóa Kho Nguyên Liệu
Vấn đề: Nhiều đầu bếp lấy nguyên liệu từ cùng một kho → Xung đột!
Ví dụ: Đếm số món đã nấu
// ❌ SAI: Nhiều đầu bếp cùng ghi vào sổ → Số liệu sai!
var soMonDaNau int
func nauMon() {
soMonDaNau++ // ⚠️ Nguy hiểm! Race condition!
}
Giải pháp: Mutex = Khóa kho, chỉ 1 người vào một lúc
Ví Dụ: Đếm Số Món An Toàn
type QuayPPhucVu struct {
mu sync.Mutex
soMonDaNau int
}
func (q *QuayPhucVu) NauXong() {
q.mu.Lock() // Khóa cửa kho
defer q.mu.Unlock() // Mở khóa khi xong
q.soMonDaNau++
fmt.Printf("✅ Đã nấu %d món\n", q.soMonDaNau)
}
func main() {
quay := &QuayPhucVu{}
var wg sync.WaitGroup
// 10 đầu bếp nấu song song
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
quay.NauXong() // An toàn với Mutex
}()
}
wg.Wait()
fmt.Printf("🎉 Tổng cộng: %d món\n", quay.soMonDaNau)
}
🔍 Giải Thích:
mu.Lock(): Khóa cửa kho - chỉ 1 đầu bếp vàodefer mu.Unlock(): Đảm bảo mở khóa khi ra- Ngăn nhiều đầu bếp ghi cùng lúc → Dữ liệu chính xác
⚠️ Những Lỗi Đầu Bếp Thường Gặp
Lỗi 1: Quên Chờ Đầu Bếp
// ❌ SAI: main() kết thúc trước khi goroutine hoàn thành
func main() {
go nauPho()
// Chương trình kết thúc ngay → Đầu bếp chưa kịp nấu!
}
// ✅ ĐÚNG: Dùng WaitGroup
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
nauPho()
}()
wg.Wait() // Chờ đầu bếp xong việc
}
Lỗi 2: Race Condition (Xung Đột Dữ Liệu)
// ❌ SAI: Nhiều goroutine ghi cùng lúc
var soMon int
go func() { soMon++ }()
go func() { soMon++ }()
// ✅ ĐÚNG: Dùng Mutex
var mu sync.Mutex
go func() {
mu.Lock()
soMon++
mu.Unlock()
}()
Lỗi 3: Tạo Quá Nhiều Goroutines
// ❌ SAI: Tạo 1 triệu đầu bếp!
for i := 0; i < 1000000; i++ {
go nauMon()
}
// ✅ ĐÚNG: Giới hạn số lượng
const maxDauBep = 10
sem := make(chan struct{}, maxDauBep)
for i := 0; i < 1000000; i++ {
sem <- struct{}{} // Chờ có chỗ trống
go func() {
nauMon()
<-sem // Giải phóng chỗ
}()
}
💡 Bí Quyết Của Đầu Bếp
- Luôn dùng WaitGroup hoặc Channels để đồng bộ
- Dùng Mutex khi nhiều goroutines truy cập dữ liệu chung
- Giới hạn số lượng goroutines - đừng tạo quá nhiều!
- defer Unlock() để đảm bảo luôn mở khóa Mutex
🎓 Bạn Đã Học Được
- ✅ Goroutine = Đầu bếp song song, làm việc độc lập
- ✅ Từ khóa
gođể tạo goroutine - ✅ WaitGroup để điểm danh và chờ đầu bếp
- ✅ Mutex để bảo vệ dữ liệu chung (kho nguyên liệu)
- ✅ Tránh race conditions và lỗi thường gặp
🍜 Món Tiếp Theo
Goroutines giúp nhiều đầu bếp làm việc song song, nhưng họ cần giao tiếp!
👉 Băng Chuyền Món Ăn - Channels - Học cách đầu bếp truyền món cho nhau!
💡 Lời Khuyên Cuối: Goroutines là đặc sản của Go - sức mạnh thật sự của Bếp Á! Hãy thực hành nhiều để thành thạo cách điều phối "đội bếp" của bạn! 👨🍳👨🍳👨🍳