Go Zero to Hero - Bài 59: Đồng bộ hoá cơ bản với sync.Mutex
1. Mutex là gì? Quy luật của Phòng thử đồ
Mutex viết tắt của Mutual Exclusion (Loại trừ lẫn nhau).
Hãy tưởng tượng biến ticketCount của bạn là một cái Phòng thử đồ trong shop quần áo, và cái phòng này chỉ có đúng 1 chiếc chìa khóa:
- Khi Goroutine A muốn vào phòng (thay đổi biến
ticketCount), nó phải lấy chìa khóa và Khóa cửa lại (Lock). - Lúc này, Goroutine B, C, D chạy tới. Thấy cửa khóa, tất cả BẮT BUỘC phải đứng xếp hàng chờ (Block) ở bên ngoài.
- Khi Goroutine A thay đồ xong (cộng số xong), nó mở cửa bước ra và Trả lại chìa khóa (
Unlock). - Ngay lập tức, Goroutine B chộp lấy chìa khóa, khóa cửa lại và lặp lại quy trình.
Nhờ cái chìa khóa (Mutex) này, tại một tíc tắc, Go đảm bảo chỉ có duy nhất 1 Goroutine được phép đụng vào vùng nhớ đó. Race Condition bị tiêu diệt hoàn toàn!
2. Thực chiến: Sửa lỗi bốc hơi vé bằng Mutex
Chúng ta sẽ dùng sync.Mutex để sửa lại đoạn code bán vé lỗi ở Bài 58. Mutex cung cấp đúng 2 hàm cực kỳ đơn giản: Lock() và Unlock().
package main
import (
"fmt"
"sync"
)
func main() {
var ticketCount int = 0
var wg sync.WaitGroup
// Khai báo một chiếc ổ khóa
var mu sync.Mutex
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 1. KHÓA CỬA: Yêu cầu độc quyền truy cập
// Nếu có luồng khác đang giữ khóa, luồng này sẽ đứng khựng lại ở đây để chờ
mu.Lock()
// 2. THAO TÁC AN TOÀN: Lúc này 100% chỉ có 1 mình luồng này được chạy lệnh ++
ticketCount++
// 3. MỞ CỬA: Xong việc phải trả chìa khóa cho luồng khác vào
mu.Unlock()
}()
}
wg.Wait()
// Kết quả BẤT BẠI: Luôn luôn là 1000 trong mọi lần chạy!
fmt.Println("Tổng số vé đã bán (An toàn tuyệt đối):", ticketCount)
}
Thử chạy lại bằng go run -race main.go, trình biên dịch sẽ hoàn toàn im lặng. Code của bạn đã miễn nhiễm với Data Race.
3. Tuyệt kỹ Backend: Đóng gói Mutex vào Struct
Trong các dự án thực tế, người ta hiếm khi vứt một biến Mutex trơ trọi ra ngoài biến toàn cục hay nhét tạm vào hàm main. Nếu dự án có chục biến cần khóa, bạn sẽ không biết ổ khóa nào dùng cho biến nào!
Tiêu chuẩn Clean Code: Biến nào cần bảo vệ, hãy nhét biến đó và cái ổ khóa bảo vệ nó vào cùng một Struct.
Hãy xem cách thiết kế một bộ đếm (SafeCounter) chuẩn mực cho máy chủ AFC:
package main
import (
"fmt"
"sync"
)
// Đóng gói Dữ liệu và Khóa vào chung một thực thể
type SafeCounter struct {
mu sync.Mutex
count int
}
// Hàm tăng biến đếm (Bảo vệ bằng Pointer Receiver)
func (c *SafeCounter) Increment() {
c.mu.Lock()
// DÙNG DEFER ĐỂ UNLOCK: Đây là luật bất thành văn!
// Đảm bảo chắc chắn khóa sẽ được mở kể cả khi hàm bị lỗi (panic) ở giữa chừng
defer c.mu.Unlock()
c.count++
}
// Hàm đọc biến đếm (Đọc cũng phải khóa để tránh lấy nhầm dữ liệu đang được ghi)
func (c *SafeCounter) GetValue() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
// Giả lập 500 cổng xoay cùng gửi tín hiệu
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment() // Gọi method an toàn
}()
}
wg.Wait()
fmt.Println("Giao dịch ghi nhận:", counter.GetValue())
}
4. Những cái bẫy "Sập Server" với Mutex
Dùng Mutex thì dễ, nhưng dùng sai thì hậu quả còn kinh hoàng hơn cả Race Condition.
-
Bẫy 1: Quên Unlock (Deadlock Vĩnh Cửu) Nếu bạn lấy khóa
Lock()xong, bạn thao tác dữ liệu nhưng hàm lỡ vướng lệnhreturnsớm, hoặc quên không gọiUnlock(). Cái phòng thử đồ đó sẽ bị khóa vĩnh viễn! Hàng ngàn Goroutines xếp hàng phía sau sẽ bị Block mãi mãi, kéo sập toàn bộ Server (Deadlock).👉 Cách giải quyết: Luôn luôn, ngay lập tức gõ
defer mu.Unlock()ngay dưới dòngmu.Lock(). -
Bẫy 2: Copy Mutex (Vứt đi ổ khóa gốc) Ở Bài 35 chúng ta đã học truyền tham trị (Pass-by-value) sẽ copy toàn bộ Struct. Nếu bạn truyền một Struct chứa Mutex vào một hàm mà KHÔNG dùng con trỏ, Go sẽ tạo ra một bản copy của cái ổ khóa đó. Lúc này, luồng A khóa cái ổ khóa gốc, nhưng luồng B lại khóa cái "ổ khóa copy". Kết quả: Vùng nhớ vẫn bung bét như chưa hề có cuộc chia ly.
👉 Cách giải quyết: Nếu Struct có chứa
sync.Mutex, BẮT BUỘC phải truyền nó đi bằng Con trỏ (*Struct). (Như ví dụSafeCounterở phần 3).
💡 Tổng kết
Bạn đã nắm trong tay phương pháp đồng bộ hóa mạnh mẽ và nguyên thủy nhất của máy tính:
- Chức năng:
sync.Mutexđảm bảo chỉ có 1 Goroutine được thao tác với một khối code/vùng nhớ tại một thời điểm. - Cú pháp: Kẹp vùng nguy hiểm vào giữa
mu.Lock()vàmu.Unlock(). - Thực chiến: Luôn đóng gói Mutex và biến cần bảo vệ vào chung một Struct. Luôn dùng
defer mu.Unlock()để bảo vệ mạng sống cho Server. - Luật con trỏ: Không bao giờ được phép copy một biến
sync.Mutex(luôn truyền bằng con trỏ).
All rights reserved