Giới thiệu về Goroutines
Sau một thời gian dài làm việc với Golang mà không chia sẻ về Goroutines
thì thật thiếu sót. Lần này mình sẽ đi qua một vài khái niệm đơn giản trước, giúp bạn làm quen với Goroutines
.
Goroutines
Bắt đầu với chương trình đơn giản, ta có một danh sách words
và muốn in chúng ra màn hình:
words := []string{
"one",
"two",
"three",
}
for i, w := range words {
fmt.Println(i, w)
}
// output
one
two
three
Chương trình đang chạy tuần tự, nghĩa là phải in one
xong thì mới đến lượt two
và sau đó là three
. Hmm, giờ làm sao in ra mà không phải đợi nhỉ?
-> Lúc này ta đến với khái niệm Goroutines
, chỉ đơn giản thêm từ khóa go
vào trước fmt.Println
thôi:
words := []string{
"one",
"two",
"three",
}
for _, w := range words {
go fmt.Println(w)
}
// output
two
one
Oh, chương trình đã chạy xong nhưng three
đâu rồi? Thử chạy lại một vài lần thì kết quả hoàn toàn khác nhau. It’s flaky!
Vậy giờ sửa lỗi này như nào? Thử đợi một giây xem nào:
words := []string{
"one",
"two",
"three",
}
for _, w := range words {
go fmt.Println(w)
}
time.Sleep(time.Second)
// output
one
three
two
Bây giờ kết quả đã đủ các giá trị nhưng nó không hề in theo thứ tự, vì sao?
-> Bởi vì Goroutines
thực thi các lệnh độc lập và đồng thời, vì vậy mà không thể bảo đảm thứ tự.
WaitGroups
Tuy nhiên trong thực tế, khi một function thực thi, chúng ta không thế nào biết được phải đợi bao lâu. Ví dụ:
printAny := func(value any) {
time.Sleep(2 * time.Second) // assume we do a heavy processing which takes 2 seconds
fmt.Println(value)
}
words := []string{
"one",
"two",
"three",
}
for _, w := range words {
go printAny(w)
}
time.Sleep(time.Second)
// output
Lúc này chẳng có output nào cả, vì printAny
mất hơn 2s mỗi khi thực thi trong khi ta chỉ đợi có 1s. Vậy làm sao để giải quyết vấn đề này?
-> Đến lúc giới thiệu WaitGroups rồi:
var wg sync.WaitGroup
printAny := func(value any) {
time.Sleep(2 * time.Second) // assume we do a heavy processing which takes 2 seconds
fmt.Println(value)
wg.Done() // make this function complete by decreasing the WaitGroup by one
}
words := []string{
"one",
"two",
"three",
}
wg.Add(len(words)) // define the number of goroutines that will run
for _, w := range words {
go printAny(w)
}
wg.Wait() // wait for all goroutines complete
// output
two
three
one
Kết quả đúng như mong đợi, WaitGroups
giúp ta chờ các goroutines
hoàn tất bằng cách:
- Đầu tiên ta phải khai báo số lượng
goroutines
sẽ chạy vớiwg.Add
, hay gọi làcounter
đi. - Khi một chức năng thực thi xong, giảm
counter
lại bằng cách gọiwg.Done
. - Cuối cùng,
wg.Wait
giúp khóa chương trình cho đến khi tất cả chức năng thực thi xong (chờcounter
về 0).
Race Condition and Mutex
Tuy nhiên, như bao chương trình xử lý đồng thời khác, ta sẽ đối mặt với vấn đề race condition
. Cùng tạo một function đơn giản updateName
để reproduce
nào.
var wg sync.WaitGroup
name := "Qui"
updateName := func(newName string) {
fmt.Printf("Start updating from '%s' to '%s'\n", name, newName)
name = newName
wg.Done()
}
wg.Add(3)
go updateName("Khanh Qui")
go updateName("Khanh Qui Vo")
go updateName("Steve")
wg.Wait()
fmt.Printf("Final result: '%s'\n", name)
// output
Start updating from 'Qui' to 'Steve'
Start updating from 'Qui' to 'Khanh Qui Vo'
Start updating from 'Qui' to 'Khanh Qui'
Final result: 'Khanh Qui'
Xem nào, tên đã được cập nhật thành công. Nhưng khoan đã, tại sao updateName
luôn Start updating from 'Qui'
, tức là nó luôn access giá trị cũ mỗi khi đổi tên.
-> Vấn đề này gọi là race condition
vì nhiều goroutines
access cùng một giá trị và cùng một thời điểm.
Điều này cực kì nguy hiểm, nếu không cẩn thận nó có thể passed test dễ dàng. Một khi viết code ta nên expect chương trình:
updateName
cập nhật được tên.updateName
phải access giá trị mới nhất rồi mới thực hiện update.
Hmm, cùng chỉnh sửa một xíu để sửa lỗi này nào:
var wg sync.WaitGroup
name := "Qui"
updateName := func(newName string) {
fmt.Printf("Start updating from '%s' to '%s'\n", name, newName)
name = newName
wg.Done()
}
wg.Add(1)
go updateName("Khanh Qui")
wg.Wait()
wg.Add(1)
go updateName("Khanh Qui Vo")
wg.Wait()
wg.Add(1)
go updateName("Steve")
wg.Wait()
fmt.Printf("Final result: '%s'\n", name)
// output
Start updating from 'Qui' to 'Khanh Qui'
Start updating from 'Khanh Qui' to 'Khanh Qui Vo'
Start updating from 'Khanh Qui Vo' to 'Steve'
Final result: 'Steve'
Và giờ chương trình chạy đúng như ta mong đợi, nhưng goroutines phải đợi các thèng khác hoàn thành trước. Nói cách khác, nó không khác gì một chương trình chạy tuần tự bình thường cả, chỉ là nhìn phức tạp hơn thôi 😂
-> Đây là lúc giới thiệu Mutex
- a mutual exclusion lock
var wg sync.WaitGroup
var lock sync.Mutex
name := "Qui"
updateName := func(newName string) {
lock.Lock()
fmt.Printf("Start updating from '%s' to '%s'\n", name, newName)
name = newName
lock.Unlock()
wg.Done()
}
wg.Add(3)
go updateName("Khanh Qui")
go updateName("Khanh Qui Vo")
go updateName("Steve")
wg.Wait()
fmt.Printf("Final result: '%s'\n", name)
// output
Start updating from 'Qui' to 'Steve'
Start updating from 'Steve' to 'Khanh Qui Vo'
Start updating from 'Khanh Qui Vo' to 'Khanh Qui'
Final result: 'Khanh Qui'
Yah đây là cái ta thật sự muốn. Chương trình chạy song song, và mỗi khi access một tài nguyên chung, nó phải đợi Unlock
thì mới được phép access. Cái này còn gọi là thread-safe.
Đến đây bài cũng dài, hẹn anh chị em dịp sau nhe, cảm ơn mọi người đã đọc đến đây.