[Golang] Tìm hiểu và xử lý race condition bằng atomic, mutex cùng Hìn béo

— Anh Khấc này!

— Ủa Hìn hả. Hai tuần rồi sao không thấy mặt mũi đâu?

— Tại thằng Phong nó mải cày phim quá, không chịu viết bài đó.


Một phần của series “Đại ca Phong học Golang“.

Tóm tắt kiến thức

  • Race condition: hiện tượng nhiều tiến trình cùng truy cập và muốn thay đổi giá trị của biến, nhưng không theo quy tắc nào, khiến kết quả không như mong muốn.
  • Golang xử lý race condition bằng việc dùng atomic hoặc mutex hoặc channel.
  • Waitgroup: là công cụ để quản lý luồng chạy của goroutines : go routines chính đợi cho tới khi các goroutines con có tín hiệu xử lý xong mới chạy tiếp
  • Atomic: package của Go, dùng để đảm bảo tại 1 thời điểm, chỉ có 1 tiến trình duy nhất được đọc/ghi giá trị vào một biến. Atomic chỉ support các kiểu số nguyên (int32, int64, uint32,…)
  • Mutex: (đọc là “mu tếch”) gần giống với atomic. Khác ở chỗ atomic chỉ lock 1 biến, còn mutex lock cả 1 đoạn code;
  • Channel: Cái này dài, bài sau nêu chi tiết hơn.
Hình ảnh thú vị bên medium về race condition tớ search được

Chém

— 2 tuần rồi Hìn học thêm được nhiều thứ chưa?

— Đang định khoe với anh nè. Tuần rồi Hìn học được về Wait Group, không cần xài sleep nữa nhé.

— Ghê thiệt. Show hàng, à nhầm, show code anh coi.

package main
import (
"fmt"
"sync"
"time"
)
func test1(wg *sync.WaitGroup) {
fmt.Println("Chạy hàm test 1")
// Oánh dấu đã done
wg.Done()
}
func test2(wg *sync.WaitGroup) {
fmt.Println("Chạy hàm test 2")
// Oánh dấu đã done
wg.Done()
}
func main() {
// Khai báo 1 wait group
var wg sync.WaitGroup
// Set giá trị counter = 2
wg.Add(2)
go test1(&wg)
go test2(&wg)
// Block main goroutines chạy tiếp, khi nào counter = 0 thì mới cho chạy q
wg.Wait()
fmt.Println("Good bye")
}

view rawvi_du_wait_group.go hosted with ❤ by GitHub

— Em giải thích kĩ hơn về Wait Group nhé:
Wait Group thường được dùng để đợi một tập các goroutines hoàn thành xử lý xong mới cho goroutines chính chạy tiếp.
+ Tưởng tượng Wait Group giống như một bộ đếm vậy. Khi nào giá trị của bộ đếm về 0 thì coi như xong, không cần đợi nữa.
+ Trong ví dụ trên, ta khởi tạo 1 wait group với giá trị bộ đếm là 2: wg.Add(2), tương đương với 2 goroutines.
+ Sau đó, trong các goroutines, khi xử lý xong sẽ gọi hàm Done() để giảm giá trị counter đi.
+ Khi giá trị của counter > 0, hàm wg.Wait() sẽ block goroutines đang gọi hàm này lại. Khi nào counter về 0 thì mới nhả ra để chạy tiếp.

— Đù. Hìn sắp join team dev đến nơi rồi.

— À, em có thử viết một ví dụ khác với Goroutines mà kết quả cũng không như ý muốn. Anh xem nè

package main
import (
"fmt"
"sync"
)
var x int64 = 0
func addOne(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go addOne(&w)
}
w.Wait()
fmt.Println("Giá trị của x là: ", x)
}

view rawrace_condition.go hosted with ❤ by GitHub

— Trong ví dụ trên, em tạo 1000 goroutines khác nhau. Mỗi goroutines có nhiệm vụ tăng giá trị của x thêm 1. Kết quả mong muốn là 1000. Nhưng khi em chạy thử thì kết quả nhận được lạ lắm: lúc ra 1000, lúc ra 981, 845, 838… linh tinh lắm. Không cố định như tình cảm của Hìn dành cho anh Khấc đâu.

— À, của em bị race condition rồi.

— Race condition? Là gì hả anh?

— Race condition, hay gọi ngầu hơn là “tương tranh”, là tình trạng các tiến trình cùng truy cập một nguồn tài nguyên và cùng thay đổi chúng. Nhưng việc truy cập lại không có trình tự, dẫn đến sai lệch kết quả so với mong muốn. Race condition là bài toán thường gặp trong lập trình đa luồng.

— Em chưa thông lắm. Như ví dụ trên của em, tại sao kết quả lại ra khác nhau ở mỗi lần chạy như vậy ?

— Để anh vẽ ra cho em dễ hiểu

Giải thích về race condition

— Ví dụ trên có 2 goroutines cùng xử lý việc tăng giá trị của biến x thêm 1 đơn vị. Để làm được điều này, goroutines cần xử lý 3 bước: đọc giá trị hiện tại của biến x, tăng thêm 1, ghi lại vào biến x.

— Oh, như hình trên thì 2 goroutines G1 và G2 cùng đọc ra giá trị của x là 0, cùng tăng lên 1 đơn vị, sau đó ghi vào cùng giá trị là 1. Đúng ra khi G1 thay đổi xong rồi thì G2 mới được đọc ra.

— Đúng là như thế.

— Vậy làm thế nào để tránh được race condition và đưa ra kết quả đúng được ạ?

— Hỏi hay lắm. Go có nhiều cách để tránh race condition. Điển hình nhất có thể kể đến dùng channel, atomic và mutex. Để anh ứng dụng vào ví dụ của Hìn, rồi giải thích chi tiết cho cả bạn đọc cùng hiểu nữa nhé.

— Sử dụng với atomic, thì anh sẽ sửa thành như sau:

package main
import (
"fmt"
"sync"
"sync/atomic"
)
var x int64 = 0
func addOne(wg *sync.WaitGroup) {
// Xài hàm của atomic để tăng giá trị.
//x = x + 1
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go addOne(&w)
}
w.Wait()
fmt.Println("Giá trị của x là: ", x)
}

view rawfix_rc_with_atomic.go hosted with ❤ by GitHub

— Trong ví dụ trên, anh dùng hàm AddInt64 để tăng giá trị của 1 biến kiểu int64. Việc xử lý đồng bộ sẽ do atomic xử lý. Chỉ cần gọi thôi.

— Ủa, ví dụ mà kiểu dữ liệu không phải là int64 thì sao hả anh?

— atomic có support một số kiểu dữ liệu khác như int64int32uint64uint32,… nói chung toàn là kiểu nguyên. Đối với các kiểu dữ liệu khác thì chúng ta sẽ dùng mutex hoặc channel.

— Mutex? Nghe giống Kote… thế 

— Hìn bậy bạ giống anh thiệt. Với mutex, anh sẽ sửa lại chương trình như sau:

package main
import (
"fmt"
"sync"
)
var x int64 = 0
// Khai báo mutex
var mutex = &sync.Mutex{}
func addOne(wg *sync.WaitGroup) {
// Lock lại
mutex.Lock()
x = x + 1
// Unlock
mutex.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go addOne(&w)
}
w.Wait()
fmt.Println("Giá trị của x là: ", x)
}

view rawfix_rc_with_mutex.go hosted with ❤ by GitHub

— Như em thấy, mutex giống như 1 cái khóa vậy. Chỗ nào cần chạy đồng bộ, tức là chỉ duy nhất 1 tiến trình được phép xử lý, thì mình dùng hàm Lock() để lock lại. Xong đoạn đó thì dùng hàm Unlock() để các tiến trình khác có thể nhào vô kiếm ăn.

— Hay nhỉ. Thế thì không bị threesom.., à nhầm, chạy lẫn lộn. Ủa mà còn cái channel gì đó thì sao anh?

— Channel hơi dài, nên để tới bài sau anh giải thích nhé bé.

Lời cảm ơn

Như thường lệ, cảm ơn Meo đã giúp anh review bài viết ^^

Cảm ơn bạn đã bỏ thời gian nghe Phong chém. Hi vọng bài viết sẽ giúp ích được cho bạn.

Nếu có gì chưa đúng trong bài thì hãy comment cho Phong biết nhé.

Have a good night ^^!