Concurrency trong Go Lang
Goroutines (note)
Một chương trình chạy có thể có một hoặc nhiều goroutines. Tuy nhiên hàm main lại đặc biệt hơn. Khi hàm main() exit, tất cả các goroutines lập tức bị terminate.
Từ đó sinh ra một khái niệm mới: WaitGroup.
Một WaitGroup sẽ chờ một tập hợp goroutines kết thúc. Hàm goroutines chính sẽ thêm số goroutines mà nó muốn chờ, mỗi hàm goroutine khi chạy xong sẽ gọi Done(). Cho tới khi mà các goroutines chưa được chạy xong, thì waitgroup sẽ block chương trình tại thời điểm đó.
func main() {
var message []int
var wg sync.WaitGroup //tạo instance
wg.Add(3) // Thêm 3 goroutines vào danh sách muốn đợi
go func() {
defer wg.Done() // sau khi chạy 2 lệnh dưới xong sẽ kết thúc,
// trả về done cho wg
time.Sleep(time.Second * 3)
messages[0] = 1
}()
go func() {
defer wg.Done()
time.Sleep(time.Second * 2)
messages[1] = 2
}()
go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
messages[2] = 3
}()
go func() {
for i := range messages {
fmt.Println(i)
}
}()
wg.Wait() // chừng nào chưa chạy xong chưa chạy xong 3 hàm trên,
// block chương trình.
}
Chúng ta có thể hiểu như sau: Khi wg(WaitGroup) Add n goroutines để đợi, với mỗi goroutine chạy xong, wg sẽ giảm đi 1. Hàm wg.Wait() chỉ được chạy qua khi và chỉ khi wg có số goroutines để đợi bằng 0.
Thử viết lại hàm lúc nãy bằng waitgroup:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Hello")
}()
wg.Wait()
Xử lý xung đột
Khi hai hay nhiều goroutine xử lý cùng 1 biến hay tài nguyên thì chuyện xảy ra xung đột là không tránh khỏi vì đọc/ghi của các goroutine không theo thứ tự mong muốn. Xử lý xung đột là một trong những vấn đề phức tạp nhất của xử lý đồng thời bởi khả năng gây là lỗi là rất cao. Để đảm bảo không xung đột, biến hay tài nguyên cần phải được đồng bộ khi xử lý và đảm bảo tại một thời điểm chỉ có một goroutine sử dụng biến hay tài nguyên này.
Để xử lý xung đột, Go cung cấp 2 package atomic và sync giúp chúng ta thực hiện việc này dễ dàng.
Atomic
Package atomic tạo cơ chế khóa cấp thấp cho các dữ liệu cần bảo vệ kiểu số và kiểu con trỏ với các hàm:
Mutex
– Thêm package “sync” vào import.
var ( counter int64 mutex sync.Mutex ) mutex.Lock() counter++ mutex.Unlock()
Tóm tắt:
Defer
Một số lưu ý khi sử dụng defer:
Giá trị tham số của hàm được gọi kèm với defer sẽ nhận giá trị tại thời điểm defer được gọi chứ không phải thời điểm hàm này thực thi.
func a() { i := 0 defer fmt.Println(i) i++ return }
Khi thực thi thì giá trị i được in ra là 0 do lúc gọi defer thì hàm Println nhận giá trị i = 0 nên dù thực hiện cuối hàm nó vẫn in giá trị 0.
– Khi có nhiều defer trong một hàm thì các lệnh defer thực hiện theo cơ chế, gọi sau thực hiện trước. Trong ví dụ bên dưới thì kết quả in ra là “3210”
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }
– Do defer thực hiện sau cùng ở hàm, sau cả lệnh return nên nếu sử dụng defer hàm vô danh thì hàm vô danh có thể nhận giá trị biến trả về mới nhất.
func double(x int) (result int) { defer func() { fmt.Printf("double(%d) = %d\n", x, result) }() return x + x }
for _, filename := range filenames { f, err := os.Open(filename) if err != nil { return err } defer f.Close() // Nguy hiểm, có thể gây cạn tài nguyên // ...xử lý f... }
+ Là lệnh khai báo với từ khóa defer giúp lệnh không thực thi ngay mà thực thi vào cuối hàm, sau cả lệnh return.
+ Defer thường dùng để báo đóng/hủy tài nguyên đã sử dụng.
+ Không nên dùng defer đóng tài nguyên trong vòng for vì sẽ tạo ra hàng loạt lệnh thực thi cuối hàm dẫn đến ngốn tài nguyên.