Chuyển tới nội dung chính

👨‍🍳 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 danh
  • defer wg.Done(): Đánh dấu "đã xong" khi món nấu xong
  • wg.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ào
  • defer 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

  1. Luôn dùng WaitGroup hoặc Channels để đồng bộ
  2. Dùng Mutex khi nhiều goroutines truy cập dữ liệu chung
  3. Giới hạn số lượng goroutines - đừng tạo quá nhiều!
  4. 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! 👨‍🍳👨‍🍳👨‍🍳