Trong bài viết dưới đây, chúng ta sẽ cùng tìm hiểu về bản chất của Goroutines cũng như cách khai báo và chi tiết cách sử dụng Goroutines. Đầu tiên chúng ta cùng tìm hiểu qua về định nghĩa Goroutines là gì trước nhé!
Goroutines là gì?
Goroutines là một trong những tính năng đặc biệt nhất của Golang để lập trình Concurrency cực kỳ đơn giản. Goroutines bản chất là các hàm (function) hay method được thực thi một các độc lập và đồng thời nhưng vẫn có thể kết nối với nhau. Một cách ngắn gọn, những thực thi đồng thời được gọi là Goroutines trong Go (Golang).
Bản chất của Goroutine
Trong bất kỳ một chương trình Golang đều tồn tại ít nhất một Goroutine, gọi là main Goroutine. Nếu main goroutines này kết thúc, toàn bộ các goroutines khác trong chương trình cũng đều bị dừng và kết thúc ngay.
Goroutine bản chất là một lightweight execution thread (luồng thực thi gọn nhẹ). Vì thế việc sử dụng các Goroutines trong Golang có chi phí cực kì thấp so với cách sử dụng các Thread truyền thống (OS Thread).
Goroutine so với Thread
- Goroutines có kích thước nhỏ hơn rất đáng kể so với Thread. Goroutines sử dụng 2KB memory stack, trong khi đó các OS Thread lên đến 2MB.
- Goroutines có thể linh động tăng giảm bộ nhớ sử dụng trong khi đó OS Thread là cố định.
- Một chương trình Golang có thể có hàng trăm nghìn Goroutine trong khi Thread chỉ được vài trăm đến hàng nghìn.
- Goroutines có thời gian khởi động nhanh hơn Thread.
- Goroutines có thể giao tiếp an toàn với nhau thông qua các kênh trong Golang (Channel). Các channel hỗ trợ mutex lock vì thế tránh được các lỗi liên quan tới cùng ghi và đọc lên vùng dữ liệu chia sẻ (data race).
- Goroutines có thể được ánh xạ và hoạt động trên nhiều OS threads thay vì là ánh xạ
1:1
.
Cách khai báo một Goroutine
Bất kì một hàm nào trong Golang cũng đều có thể chạy đồng thời hay Goroutines với việc thêm vào từ khoá go
.
Từ khoá go
cũng chính là tên của ngôn ngữ Go, là first-class keyword. Nghĩa là trong Golang bạn không cần phải import bất kì một package nào để sử dụng nó.
Trong ví dụ trên, hàm sayHello("Viet")
có từ khoá go
phía trước nên sẽ là một Goroutine. Hàm này sẽ trả về ngay lập tức chứ không thực thi ngay. Sau đó hàm sayHello("Nam")
được thực thi bình thường bởi main goroutine. Kết quả chúng ta thấy được "Hello Nam" được in ra 5 lần trên màn hình.
Vậy tại sao không thấy "Hello Viet" nào cả?! Vì khi kết thúc hàm sayHello("Nam")
, main goroutine đã kết thúc và chương trình exit trước khi các Goroutine bên dưới nó được thực thi.
200Lab sẽ cho chương trình Sleep một giây trước khi kết thúc:
Note: Trong ví dụ trên sẽ xuất hiện trường hợp các không thấy bất kỳ dòng "Hello Viet" nào hoặc không đủ 5 lần. Nguyên nhân là trong đoạn code trên không có gì đảm bảo rằng hàm sayHello("Viet") chắc chắn sẽ được call sau khi sleep 1s. Chỗ này còn tuỳ vào runtime.
Để giải quyết triệt để vấn đề trên, chúng ta buộc phải dùng các phương thức đồng bộ như Mutext Lock hoặc cơ chế Channel block của Golang. Tuy nhiên vì để giữ tính đơn giản cho bài này, 200Lab sẽ sử dụng đoạn code trên.
Sử dụng Goroutine với anonymous function Golang
Chúng ta có thể khai báo goroutine cho một đoạn code như sau:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
for i := 0; i <= 5; i++ {
fmt.Println(i)
}
}()
time.Sleep(time.Second)
}
Một hàm vô danh (anonymous function) được thực thi ngay có dạng func(){ ... }()
. Khi thêm go
vào trước nó sẽ được một anonymous Goroutine. Cực kỳ đơn giản.
Vấn đề capture variable trong Goroutine Golang
Đây là một vấn đề mà rất nhiều Gopher (Golang developer) gặp phải khi sử dụng Goroutines. Chúng ta thử xem xét đoạn code dưới đây nha:
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i <= 100; i++ {
go func() {
fmt.Println(i) // biến i ở đây là một pointer
}()
}
time.Sleep(time.Second)
}
Khi chạy đoạn code trên bạn sẽ phát hiện rất nhiều giá trị i
bị trùng lặp. Đúng ra phải là các giá trị từ 1
đến 100
được in ra theo thứ tự ngẫu nhiên và không trùng.
Nếu một hàm sử dụng biến ở ngoài nó thì biến đó sẽ được capture, hay có thể hiểu chỉ là một tham chiếu đến biến ở ngoài mà thôi, không phải giá trị. Vì các Goroutines không được thực thi ngay, dẫn đến giá trị i ở ngoài thay đổi thì khi thực thi nó sẽ in ra i đã bị thay đổi.
Để tránh điều này, chúng ta sẽ copy giá trị i cho từng function Goroutine:
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i <= 100; i++ {
go func(value int) {
fmt.Println(value) // value ở đây độc lập với i ở ngoài
}(i) // value i được copy ở đây
}
time.Sleep(time.Second)
}
Kết quả sẽ là 1-100 in ra thứ tự ngẫu nhiên và không trùng nhau.
Sử dụng Gosched() để force schedule Goroutines
Việc thực thi và phân bổ tài nguyên giữa các Goroutines được runtime Golang quản lý. Trong đại đa số trường hợp chúng ta không cần phải quan tâm đến nó.
Dưới đây sẽ là một cách để tránh một Goroutine chiếm process khi làm việc với vòng lặp. Hãy cùng xem xét ví dụ sau đây:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
for i := 1; i <= 50; i++ {
fmt.Println("I am Goroutine 1")
}
}()
go func() {
for i := 1; i <= 50; i++ {
fmt.Println("I am Goroutine 2")
}
}()
time.Sleep(time.Second)
}
Khả năng rất cao các bạn sẽ thấy "I am Goroutine 1" hoặc "I am Goroutine 2" in ra liên tục mới đến cái còn lại.
Thực ra đây cũng là vấn đề thường thấy khi review code Golang. Điều này khiến ứng dụng chạy kém hiệu quả, chiếm dụng tài nguyên lớn và gây thắc cổ chai nghiêm trọng.
Để giải quyết vấn đề này, các bạn có thể dùng 2 cách:
- Sleep một chút ở mỗi vòng lặp, chấp nhận hy sinh performance do sleep thêm thời gian.
- Sử dụng hàm
Gosched()
của package runtime để schedule goroutine ngay lập tức sau mỗi vòng lặp.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
for i := 1; i <= 50; i++ {
fmt.Println("I am Goroutine 1")
runtime.Gosched()
}
}()
go func() {
for i := 1; i <= 50; i++ {
fmt.Println("I am Goroutine 2")
runtime.Gosched()
}
}()
time.Sleep(time.Second)
}
Gosched()
chỉ giúp chúng ta schedule goroutines. Việc chọn và thực thi Goroutine nào tiếp theo là do runtime quyết định. Cách này sẽ rât hiệu quả nếu bạn có rất nhiều Goroutines trong chương trình đang làm việc với vòng lặp.
Lời kết
Hy vọng qua bài viết này sẽ giúp các bạn hiểu rõ hơn về Goroutine, tự tin ứng dụng trong các service cũng như các buổi phỏng vấn tuyển dụng Golang Developer.
Còn nếu bạn thực sự cảm thấy khó khăn trong việc tự học. Thậm chí đã làm được những service cơ bản nhưng vẫn chưa tự tin cho những phần nâng cao thì có thể tham khảo khoá học Golang for Scalable Backend tại 200Lab nhé!
Tham khảo thêm:
- Những sai lầm thường thấy khi bạn sử dụng goroutines
- Golang Channel là gì? Các ứng dụng Channel trong Golang
- Buffered Channel là gì? Thường xuyên bị hỏi trong phỏng vấn Golang Dev
- Generics trong Golang
Việt Trần
Yêu thích tìm hiểu các công nghệ cốt lõi, kỹ thuật lập trình và thích chia sẻ chúng tới cộng đồng
follow me :
Bài viết liên quan
Design Pattern là gì? 23 Classic Design Pattern với Golang
Aug 21, 2024 • 3 min read
Concurrency pattern trong Go - Phần 2
Apr 11, 2024 • 7 min read
Concurrency pattern trong Golang - Phần 1
Mar 25, 2024 • 8 min read
So sánh Golang và NodeJS chi tiết
Oct 14, 2023 • 22 min read
Buffered Channel là gì? Thường xuyên hỏi trong phỏng vấn Golang Dev
Jun 14, 2023 • 6 min read
Golang Channel là gì? Các ứng dụng Channel trong Golang
Jun 14, 2023 • 12 min read