Tham khảo phần 2 ở đây
Mutex viết tắt của Mutual Exclusion. Có nghĩa là loại trừ lẫn nhau. Hiểu nôm na thế này, toilet chỉ có một buồng, nhưng cả hai ông đều muốn vào, tất nhiên sẽ phải có một ông phải đứng đợi ngoài.
Bài viết này lược dịch bài "Dancing with Go’s Mutexes" của tác giả Ralph Caraveo III.
Golang team khuyên rằng "Don't communicate by sharing memory; share memory by communicating" ~ "Đừng trao đổi bằng cách chia sẻ vùng nhớ; chia sẻ vùng nhớ bằng trao đổi". Ý nói các anh em đồng đạo hãy dùng channel để trao đổi giữa các tác vụ chạy đồng thời, chớ có dùng biến toàn cục. Tôi hiểu là nếu sử dụng channel, thì sẽ tránh được tranh chấp, khóa chết (dead lock).
Lập trình Golang thêm một thời gian, tôi lại thấy rất nhiều mã nguồn dùng thư viện sync/mutex, sync/atomic hay các chiêu thức rất cổ điển, sơ khai để đồng bộ hóa. Có lẽ developer các dòng code này chưa xem bài trình bày của cha đẻ Golang Rob Pike về ưu điểm của đồng bộ bằng channel. Khi đó Rob Pike hay nói đến ý tưởng thiết kế ảnh hưởng từ bài viết Communicating Sequential Processes của Tony Hoare.
Vọc vào code của nhiều dự án mã nguồn mở Golang, tôi thấy mutex (mutual exclusion) được dùng rất nhiều. Tình cờ tìm được tài liệu "Mutex hay là Channel" của các cao thủ trong team Golang của Google viết như sau:
Nếu phải lựa chọn giữa Mutex và Channel, hãy chọn cách nào dễ biểu đạt (code sáng) và code dễ nhất.
Một lỗi thường thấy của lập trình Golang mới vào nghề là lạm dụng channels và goroutines bởi vì nó khả thi và đúng chất Golang. Đừng ngại sync.Mutex nếu nó phù hợp với hoàn cảnh bài toán của bạn.
Dùng Channels khi truyền, trao quyền sử dụng dữ liệu (passing ownership of data), phân phối một đầu việc (distributing units of work) và trao đổi kết quả bất đồng bộ (communicating async results).
Dùng Mutex để quản lý việc truy xuất đồng bộ vào bộ đệm (cache) hay trạng thái (state).
Điều 1: Khi khai báo một struct ở đó mutex quản lý quyền truy xuất vào một hay vài trường trong struct, hãy đặt mutext lên trên cùng.
var sum struct {
sync.Mutex // <-- this mutex protects
i int // <-- this integer underneath
}
Điều 2: Giữ mutex lock càng ngắn càng tốt, hãy mở khóa ngay khi truy xuất xong dữ liệu. Nếu khóa mutex quá lâu, bạn sẽ bắt các tác vụ đồng bộ thực thi tuần tự nối đuôi nhau.
// Cách dở
func doSomething(){
mu.Lock()
item := cache["myKey"]
http.Get() // lệnh này chả cần nằm trong lệnh khóa Mutext
mu.Unlock()
}
// Cách đúng
func doSomething(){
mu.Lock()
item := cache["myKey"]
mu.Unlock() //Mở khóa luôn khi truy xuất bộ đệm xong
http.Get() // Cho nó ra ngoài tốt hơn
}
Điều 3: Trong một hàm có nhiều điểm trả về (multiple return paths), hãy sử dùng defer Mutex.Unlock() để đảm bảo bạn không quên mở khóa Mutex, mà cũng không cần viết lệnh mở khóa trước mỗi điểm return. Như vậy defer giúp bạn khỏi bận tâm bảo trì mã nguồn kể cả sau đây 3 tháng khi đồng nghiệp thêm một nhánh rẽ (new case), code chạy vẫn ổn.
func doSomething(){
mu.Lock()
defer mu.Unlock()
err := ...
if err != nil{
//log error
return // <-- mutex sẽ mở khóa ở đây
}
err = ...
if err != nil{
//log error
return // <-- hoặc ở đây
}
return // <-- và chắc chắn ở đây cũng được mở khóa
}
Nhớ rằng defer chỉ hoạt động ở phạm vi hàm chứ không ở trong mỗi khối lệnh (block scope)
func doSomething(){
for {
mu.Lock()
defer mu.Unlock()
// <-- defer không hoạt động ở phạm vi trong vòng lặp for
}
// <-- defer hoạt động khi hàm thoát thôi
}
// Code trên sẽ bị dead lock khi Mutex không được unlock đầy đủ
Bình luận