Ở phần trước, tôi đã chia sẻ cho các bạn thấy cách tạo GoRoutine và cách sử dụng
sync.WaitGroup
. Với việc lập trình tương tranh trong Golang, việc tạo ra GoRoutine và chạy nó ở nền là vô cùng dễ dàng. Việc biết được vấn đề về tranh đoạt điều khiển (hay tiếng Anh là race condition) là vô cùng quan trọng. Tôi cũng sẽ chia sẻ cho bạn về mutex hay mutual exclusion (tiếng Việt là loại trừ lẫn nhau). Và cuối cùng sẽ về channel - phương pháp xử lý với tương tranh ưa thích trong go.
Giới thiệu về race condition
Tranh đoạt điều khiển xảy ra khi nhiều GoRoutine cố gắng để truy cập vào cùng dữ liệu. Điều này có thể sẽ khó có thể phát hiện khi đọc code. Trong Go, chúng ta có thể kiểm tra chúng khi chạy chương trình hoặc khi chạy test với
go test
. Chúng ta có đoạn code sau
package main import ( "fmt" "sync" ) var msg string var wg sync.WaitGroup func updateMessage(s string) { defer wg.Done() msg = s } func main() { msg = "Hello, mom!" wg.Add(2) go updateMessage("Hello, dad!") go updateMessage("Hello, sister!") wg.Wait() fmt.Println(msg) }
Khi chúng ta chạy chương trình, màn hình terminal in ra kết quả
Hello, dad!
Có thể bạn sẽ mong đợi nó in ra
Hello, sister!
bởi nó được gọi sau, nhưng bằng một cách nào đó, updateMessage("Hello, sister")
đã hoàn thành trước. Bạn cũng sẽ không thể nào biết được kết quả cho lần in ra tiếp theo . Tiếp theo, để chạy chương trình, bạn thêm
-race
vào lệnh go run
go run -race main.go
Và khi đó, Go sẽ đưa cho bạn cảnh báo về việc tranh đoạt điều khiển.
================== WARNING: DATA RACE Write at 0x0000005ab830 by goroutine 8: main.updateMessage() /home/duong/go-concurrency-1/code/main.go:14 +0x6f main.main.func2() /home/duong/go-concurrency-1/code/main.go:22 +0x37 Previous write at 0x0000005ab830 by goroutine 7: main.updateMessage() /home/duong/go-concurrency-1/code/main.go:14 +0x6f main.main.func1() /home/duong/go-concurrency-1/code/main.go:21 +0x37 GoRoutine 8 (running) created at: main.main() /home/duong/go-concurrency-1/code/main.go:22 +0x90 goroutine 7 (finished) created at: main.main() /home/duong/go-concurrency-1/code/main.go:21 +0x84 ================== Hello, sister! Found 1 data race(s) exit status 66
Mutex
Hiện tại, chương trình của tôi đang có một chút vấn đề. Hai lần gọi
updatemessage
đang xảy ra cùng một thời điểm. Chúng đang truy cập và muốn thay đổi dữ liệu của biến msg
, diễn ra đồng thời và ta không biết lần gọi nào sẽ hoàn thành trước. Chúng ta sẽ phải giải quyết vấn đề này, và may thay đối với Go, việc này là vô cùng dễ dàng.
Chúng ta sẽ sử dụng
sync.Mutex
trong code hiện tại package main import ( "fmt" "sync" ) var msg string var wg sync.WaitGroup func updateMessage(m *sync.Mutex, s string) { defer wg.Done() m.Lock() msg = s m.Unlock() } func main() { msg = "Hello, mom!" mutex := sync.Mutex{} wg.Add(2) go updateMessage(&mutex, "Hello, dad!") go updateMessage(&mutex, "Hello, sister!") wg.Wait() fmt.Println(msg) }
Như bạn có thể thấy, đầu tiên tôi khởi tạo biến
mutex := sync.Mutex{}
. Sau đó, bên trong hàm updateMessage
, tôi thêm pointer để truyền vào biến mutex trên, cùng với thêm Lock()
và Unlock()
trước và sau khi tôi truy cập và thay đổi dữ liệu của biến msg
. Sau đó, chúng ta chạy lại lện
go run -race main.go
và lần này, mặc dù chúng ta vẫn không biết GoRoutine nào sẽ hoàn thành trước, có thể là dad
hoặc sister
. Nhưng điều quan trọng ở đây là tôi đã truy cập được dữ liệu một cách an toàn. Điều này được mọi người gọi là an toàn luồng (cái tên Tiếng Việt tôi chưa kiếm được ở đâu nên bịa ra, còn người ta hay gọi là thread safe).
Hello, dad!
Bây giờ, bạn có thể thấy màn hình in ra mà không hiện
WARNING: DATA RACE
như ở trên. Go channel
Trước khi giới thiệu cho bạn về Go channel, tôi sẽ giới thiệu cho bạn một vấn đề của khoa học máy tính, có tên tiếng Việt là vấn đề Nhà sản xuất-Người tiêu thụ (Producer/Consumer Problem).
Ngắn gọn dễ hiểu, bài toán này được định nghĩa như sau: có 1
Nhà sản xuất
và 1 Người tiêu thụ
, và có 1 cái Kho
. Nhà sản xuất tạo ra món hàng, cho vào trong kho và Người tiêu thụ lấy món hàng đó ra và tiêu thụ
. Các vấn đề cần phải xem xét ở đây có thể kể đến như sau:
- Nếu cái Kho đầy thì Nhà sản xuất không được sản xuất thêm.
- Nếu cái Kho trống thì Người tiêu thụ không được tiêu thụ.
- Trong 1 thời điểm, chỉ 1 người được đụng vào cái kho.
Để mô tả bài toán trên, tôi có 1 ví dụ sau. Ta có 2 hai bạn Na và An rủ nhau vào một vườn chuối. An thèm ăn chuối, vì vậy mới bắt đầu nhờ Na hái chuối. Na hái vào một cái giỏ, An lấy chuối trong giỏ, cái giỏ có thể dựng nhiều nhất 9 quả chuối. Mỗi quả chuối có trọng lượng khác nhau và cần thời gian khác nhau để tiêu thụ. Còn với việc hái xuống và bỏ vào giỏ, đối với tất cả các kích thước, chỉ cần 1 giây. Sau đây là đoạn code minh họa cho ví dụ trên.
package main import ( "fmt" "math/rand" "time" ) // Tạo ra channel với max size là 9 var backet = make(chan int, 9) func pick() { for { // Bắt đầu tính thời gian và nhặt chuối pickTime := 1000 * time.Millisecond time.Sleep(pickTime) // Tính cân nặng và bỏ chuối vào giỏ w := rand.Intn(5) + 1 backet <- w // Đây là cú pháp để gửi tín hiệu vào channel fmt.Printf("Na HÁI chuối với trọng luợng %vkg trong vòng: %v\n", w, pickTime) } } func eat() { for { // An lấy ra quả chuối w := <- backet // Đây là cú pháp để lấy tín hiệu từ channel // Tính thời gian ăn và An bắt đầu ăn eatTime := time.Millisecond * time.Duration(w * 300) time.Sleep(eatTime) fmt.Printf("An ĂN chuối với trọng luợng %vkg trong vòng: %v\n", w, eatTime) } } func main() { rand.Seed(time.Now().UnixNano()) go pick() eat() }
Như ở trên, bạn có thể thấy cú pháp cho việc tạo ra channel, gửi và lấy dữ liệu từ channel. Cuối cùng chúng ta chạy code, màn hình in ra.
Na HÁI chuối với trọng luợng 3kg trong vòng: 1s An ĂN chuối với trọng luợng 3kg trong vòng: 900ms Na HÁI chuối với trọng luợng 5kg trong vòng: 1s Na HÁI chuối với trọng luợng 4kg trong vòng: 1s An ĂN chuối với trọng luợng 5kg trong vòng: 1.5s Na HÁI chuối với trọng luợng 1kg trong vòng: 1s An ĂN chuối với trọng luợng 4kg trong vòng: 1.2s Na HÁI chuối với trọng luợng 1kg trong vòng: 1s An ĂN chuối với trọng luợng 1kg trong vòng: 300ms An ĂN chuối với trọng luợng 1kg trong vòng: 300ms Na HÁI chuối với trọng luợng 3kg trong vòng: 1s An ĂN chuối với trọng luợng 3kg trong vòng: 900ms Na HÁI chuối với trọng luợng 5kg trong vòng: 1s Na HÁI chuối với trọng luợng 5kg trong vòng: 1s An ĂN chuối với trọng luợng 5kg trong vòng: 1.5s Na HÁI chuối với trọng luợng 4kg trong vòng: 1s Na HÁI chuối với trọng luợng 3kg trong vòng: 1s An ĂN chuối với trọng luợng 5kg trong vòng: 1.5s Na HÁI chuối với trọng luợng 3kg trong vòng: 1s An ĂN chuối với trọng luợng 4kg trong vòng: 1.2s ...
Có vẻ chương trình chạy rất tốt. Đến đây, tôi đã giới thiệu qua cho bạn về bài toán producer/consumer.