Concurrency trong Golang
Bài đăng này đã không được cập nhật trong 4 năm
1. Giới thiệu
Xin chào các bạn, hôm nay mình sẽ giới thiệu một trong những chủ đề khá quan trọng trong Golang, đó là Concurrency. Vậy concurrency trong Golang có các khái niệm gì mới không, chúng hoạt động ra sao và sử dụng như thế nào? Chúng ta sẽ cùng nhau tìm hiểu & giải đáp thông qua bài viết này
2. Các khái niệm
Trước khi đi vào các ví dụ cụ thể, chúng ta sẽ cùng tìm hiểu qua các khái niệm mới liên quan đến Concurrency trong Golang nhé
2.1. Goroutine
Nói đến concurrency, Golang cho chúng ta một khái niệm mới, đó là Goroutine. Vậy Goroutine là gì?
Goroutine là một tiểu trình nhẹ được quản lý bởi Go runtime. Goroutine là một function có khả năng chạy đồng thời với các function khác.
Cú pháp sử dụng Goroutine
go myFunction(myParam)
2.2. Channel
Một câu hỏi đặt ra, vậy làm sao để các goroutines có thể giao tiếp với nhau? Câu trả lời ở đây đó chính là channel
Channels cung cấp một cách để các goroutines giao tiếp với nhau và đồng bộ hóa việc thực thi của chúng.
Mặc định, channel là hai chiều, có nghĩa là các goroutines có thể gửi hoặc nhận dữ liệu qua cùng một channel
Cú pháp sử dụng channel
- Khai báo 1 channel:
myChannel := make(chan int)
- Gửi 1 element vào trong channel:
mychannel <- element
- Nhận element từ channel:
element := <-mychannel
- Trong trường hợp kết quả của câu lệnh nhận được sẽ không được sử dụng cũng là một câu lệnh hợp lệ. Bạn cũng có thể viết một câu lệnh nhận như sau:
<-Mychannel
3. Sử dụng Goroutine
Hãy cùng bắt đầu với 1 ví dụ cơ bản khi chưa sử dụng Goroutine.
package main
import "fmt"
import "time"
func main() {
hello("John")
hello("Peter")
}
func hello(name string) {
for i := 0; i < 5; i++ {
fmt.Println("Hello", name)
time.Sleep(time.Millisecond * 500)
}
}
Dễ dàng thấy chúng ta có 1 function hello
để in lời 5 chào đến 1 tên cụ thể cách nhau 0.5s, main function sẽ gửi lời chào đến 2 tên John & Peter
Kết quả, chúng ta sẽ nhận đc như sau:
Hello John
Hello John
Hello John
Hello John
Hello John
Hello Peter
Hello Peter
Hello Peter
Hello Peter
Hello Peter
OK, vậy giờ chúng ta cùng thay đổi thêm goroutine cho 2 lệnh hello
trong main function xem sao. Thay đổi hàm main như sau:
func main() {
go hello("John")
go hello("Peter")
}
Khi run lại bạn sẽ thấy chương trình của chúng ta kết thúc mà không in ra gì cả. Tại sao vậy?
Câu trả lời đơn giản ở đây là: trong Go, khi main function kết thúc, thì chương trình cũng kết thúc, khi đó tất cả các goroutine cũng kết thúc theo. Trong ví dụ trên, sau khi run 2 lệnh goroutine thì chương trình kết thúc luôn nên 2 goroutine không có thời gian để run. Cùng chỉnh lại chút, chúng ta sẽ sleep main function lại 1 chút sau đó kiểm tra lại bằng lệnh sau vào cuối hàm main:
time.Sleep(time.Second * 3)
Oh, Chúng ta nhận được ngay kết qua như sau:
Hello John
Hello Peter
Hello Peter
Hello John
Hello Peter
Hello John
Hello John
Hello Peter
Hello Peter
Hello John
Có thể thấy các câu "Hello John" & "Hello Peter" đã được hiển thị xen kẽ nhau chứng tỏ 2 goroutine trên đã được run bất đồng bộ đúng như mong đợi
Channel
Nào bây giờ chúng ta sẽ cũng đi đến các ví dụ về channel - cách mà các goroutine giao tiếp với nhau.
Cùng viết 1 ví dụ đơn giản như sau:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go process(c)
for {
fmt.Println("Received:", <-c)
}
}
func process(c chan int) {
for i := 1; i <= 3; i++ {
c <- i
time.Sleep(time.Millisecond * 300)
}
}
Nhìn qua ví dụ trên ta có thể thấy main
function khởi tạo 1 channel kiểu int sau đó gọi method process
để gửi 3 giá trị vào channel. Cuối cùng sử dụng hàm for để in ra các giá trị mà channel nhận được.
Cùng run đoạn code trên, ta sẽ nhận được kết quả như sau:
Received: 1
Received: 2
Received: 3
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox708494458/prog.go:13 +0x7d
Oh, 3 giá trị nhận được thì đúng rồi, nhưng sao lại có lỗi deadlock?
À, lý do ở đây là method process
sau khi gửi 3 giá trị vào channel thì đã kết thúc, trong khi phía main
function vẫn đợi giá trị tiếp theo được gửi vào trong channel, nhưng không còn gì có thể send thêm vào channel nữa, điều này dẫn đến việc main function sẽ wait forever... Tuy nhiên, go có thể phát hiện được ra vấn đề này (vấn đề runtime chứ k phải compile time) và dừng chương trình lại.
Để xử lý vấn đề này, rất đơn giản, chúng ta close channel bằng cách thêm lệnh close(c)
sau vào cuối method process
và chỉnh sửa lại vòng lặp in ra kết quả nhận được trong channel như sau:
for {
i, open := <-c
if !open {
return
}
fmt.Println("Received:", i)
}
=> Như vậy cứ mỗi lần nhận được giá trị từ channel, chúng ta kiểm tra trước xem channel đó đã được close chưa. Nếu đã close thì chúng ta cho dừng chương trình lại. Khi đó, chương trình sẽ kết thúc 1 cách bình thường và kết quả sẽ như sau:
Received: 1
Received: 2
Received: 3
Program exited.
Hoặc đơn giản hơn nữa, chúng ta chỉ cần viết vòng lặp như sau:
for i := range c {
fmt.Println("Received:", i)
}
Buffered Channels
Tiếp theo chúng ta cùng đến với ví dụ đơn giản hơn, tạo 1 channel sau đó send giá trị vào rồi lấy ra như sau:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1
fmt.Println("Received:", <-ch)
}
Kết quả như sau:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox980511399/prog.go:7 +0x59
Tại sao vậy? chúng ta mong chờ kết quả hiển thị ra bình thường giá trị 1. Tuy nhiên, ở đây có thể giải thích như sau:
khi chúng ta khởi tạo channel bằng lệnh ch := make(chan int)
thì cơ chế ở đây là mỗi khi chúng ta send 1 giá trị vào trong channel, thì goroutine sẽ block cho đến khi nào giá trị trong channel đó được lấy ra. Trong trường hợp này, chương trình chính sẽ bị block và không thể đạt tới dòng code lấy giá trị phía dưới. Và thế là Go phát hiện ra vấn về bị wait forever và lại xử lý ra lỗi deadlock.
Trong trường hợp chúng ta gửi vào channel bằng 1 goroutine & nhận channel ở 1 goroutine khác thì vẫn có thể hoạt động bình thường, goroutine gửi sẽ block cho đến khi goroutine nhận lấy giá trị trong channel ra, sau đó goroutine gửi mới tiếp tục hoạt động.
Nếu bạn muốn gửi nhiều giá trị vào trong channel thì có cách đấy, Go giới thiệu 1 dạng Buffered Channels
. Với channel dạng này chúng ta có thể chỉ định size của channel. Cùng chỉnh sửa lại đoạn code như sau:
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println("Received:", <-ch)
fmt.Println("Received:", <-ch)
}
Sử dụng câu lệnh make(chan int, 2)
có nghĩa là chúng ta đã tạo ra 1 Buffered Channels có size = 2. Sử dụng channel này chúng ta sẽ không còn phải lo goroutine sẽ bị block khi gửi message vào nữa. Kết quả như sau:
Received: 1
Received: 2
Program exited.
Dĩ nhiên nếu chúng ta cố tình send vào các giá trị nhiều hơn size của channel thì deadlock lại xảy ra.
Selection
Tiếp tục, chúng ta cùng đến với ví dụ sau:
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go sendAndSleep(c1, "Sleep 1s", time.Second*1)
go sendAndSleep(c2, "Sleep 5s", time.Second*5)
for {
fmt.Println("Received:", <-c1)
fmt.Println("Received:", <-c2)
}
}
func sendAndSleep(c chan string, value string, duration time.Duration) {
for {
c <- value
time.Sleep(duration)
}
}
Chúng ta tạo ra 2 channel & 2 goroutine, goroutine 1 sẽ gửi message "Sleep 1s" sau mỗi 1s, goroutine 2 sẽ gửi message "Sleep 5s" sau mỗi 5s, sau đó ta in ra các mesage nhận được trong channel 1 & 2. Khi run, chúng ta có kết quả như sau:
timeout running program
Received: Sleep 1s
Received: Sleep 5s
Received: Sleep 1s
Received: Sleep 5s
Received: Sleep 1s
...
Đây là 1 chương trình timeout running vì chúng ta gửi vào & in ra mãi. Tuy nhiên, hãy tạm bỏ qua vấn đề này và nhìn vào kết quả, ta sẽ thấy, mặc dù goroutine 1 gửi vào channel 1 message mỗi 1s. Nhưng main function phải đợi in xong message ở channel 2 mới có thể in tiếp message ở channel 1. Đó là lý do tại sao channel 1 được nhận mỗi 1s mà lại in ra tuần tự cùng với channel 2 là 5s.
Để không phải để channel 1 phải chờ đợi channel 2 nhận được mới xử lý tiếp, chúng ta có thể sử dụng lệnh select ở trong hàm for như sau:
for {
select {
case v1 := <-c1:
fmt.Println("Received:", v1)
case v2 := <-c2:
fmt.Println("Received:", v2)
}
}
Bằng cách này chúng ta có thể nhận được message của channel 1 mỗi khi có mà không cần phải đợi channel 2 như đoạn code phía trên. Và đây là kết quả:
timeout running program
Received: Sleep 5s
Received: Sleep 1s
Received: Sleep 1s
Received: Sleep 1s
Received: Sleep 1s
Received: Sleep 1s
Received: Sleep 5s
Received: Sleep 1s
Received: Sleep 1s
...
Kết luận
Như vậy, qua ví dụ trên mình đã giới thiệu về concurrency trong Golang, mà cụ thể ở đây là cách hoạt động của goroutine, cách sử dụng channel, select,... trong Golang. Đây chỉ là những phần khá cơ bản về concurrency trong Golang, nhưng hy vọng qua bài viết này có thể giúp các bạn hiểu được và áp dụng vào trong học tập cũng như công việc. Cảm ơn các bạn đã theo dõi bài viết!
All rights reserved