Goroutines là một trong những tính năng mạnh mẽ nhất của ngôn ngữ lập trình Golang, giúp quá trình lập trình đồng thời trở nên đơn giản hơn bao giờ hết. Trong bài viết này, chúng ta sẽ khám phá khái niệm Goroutines, sự khác biệt giữa Goroutines và Threads, cách khai báo Goroutines và những vấn đề liên quan đến việc sử dụng chúng. Qua đó, hy vọng bạn sẽ hiểu rõ hơn về cách tận dụng hiệu quả của Goroutines trong các ứng dụng thực tiễn.
Goroutines là gì?
Goroutines là gì?
Goroutines là các hàm hoặc phương thức trong Golang được thực hiện đồng thời, cho phép nhiều tác vụ xảy ra song song mà không cần phải chờ đợi nhau hoàn thành. Goroutines rất nhẹ và tiêu tốn ít tài nguyên hơn so với các Thread truyền thống, giúp cho việc lập trình đồng thời trở nên hiệu quả hơn.
Bản chất của Goroutines
Bản chất của Goroutine
Trong mọi chương trình Golang, luôn tồn tại ít nhất một Goroutine được gọi là main Goroutine. Khi Goroutine chính này kết thúc, tất cả các Goroutines khác trong chương trình sẽ dừng ngay lập tức.
Goroutines có thể được coi là lightweight execution threads (luồng thực thi nhẹ). Sử dụng Goroutines có chi phí thấp hơn rất nhiều so với việc sử dụng các Threads truyền thống, cho phép lập trình viên dễ dàng triển khai các tác vụ đồng thời.
So sánh giữa Goroutines và Threads
Goroutines so với Thread
- Kích thước: Goroutines sử dụng khoảng 2KB memory stack, trong khi đó một OS Thread có thể chiếm đến 2MB.
- Linh hoạt: Goroutines có thể tự động điều chỉnh kích thước bộ nhớ, trong khi OS Thread là cố định.
- Số lượng: Một chương trình Golang có thể khai báo hàng triệu Goroutines, trong khi số lượng Thread thường bị giới hạn ở vài trăm hoặc vài nghìn.
- Thời gian khởi động: Goroutines khởi động nhanh hơn đáng kể so với Threads.
- Giao tiếp: Các Goroutines có thể giao tiếp an toàn thông qua Channels, giúp giảm thiểu rủi ro về race conditions khi truy cập vào vùng dữ liệu chia sẻ.
Cách khai báo một Goroutine
Để khai báo một Goroutine, bạn chỉ cần thêm từ khóa go
trước bất kỳ hàm nào, ví dụ:
func name(){ // statements }
// Gọi Goroutine
go name()
Ví dụ đơn giản về Goroutine
package main
import "fmt"
func sayHello(name string) {
for i := 0; i <= 5; i++ {
fmt.Printf("Hello %sn", name)
}
}
func main() {
go sayHello("Viet") // Khởi động Goroutine
sayHello("Nam") // Hàm thực thi bình thường
}
Kết quả sẽ là:
Hello Nam
Hello Nam
Hello Nam
Hello Nam
Hello Nam
Tuy nhiên, có thể bạn sẽ không thấy “Hello Viet” xuất hiện do Goroutine sayHello("Viet")
chưa hoàn thành trước khi chương trình chính kết thúc. Để đảm bảo nó được thực thi, ta có thể thêm một khoảng thời gian chờ:
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i <= 5; i++ {
fmt.Printf("Hello %sn", name)
}
}
func main() {
go sayHello("Viet") // Khởi động Goroutine
sayHello("Nam") // Hàm thực thi bình thường
time.Sleep(time.Second) // Đợi một chút để Goroutine hoàn thành
}
Kết quả sẽ hiện lên đầy đủ cả hai lời chào.
Sử dụng Goroutines với hàm vô danh
Goroutines cũng có thể được áp dụng cho những hàm vô danh bằng cách khởi chạy một hàm không có tên như sau:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
for i := 0; i <= 5; i++ {
fmt.Println(i)
}
}()
time.Sleep(time.Second)
}
Trong trường hợp này, một Goroutine vô danh sẽ được tạo ra, thực hiện vòng lặp in số từ 0 đến 5.
Vấn đề với biến capture trong Goroutine
Một vấn đề phổ biến khi làm việc với Goroutines là biến i
được capture từ vòng lặp có thể gây ra lỗi khi giá trị của nó thay đổi. Ví dụ:
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i <= 100; i++ {
go func() {
fmt.Println(i) // sử dụng biến i từ vòng lặp
}()
}
time.Sleep(time.Second)
}
Kết quả có thể sẽ không như mong đợi vì i
sẽ luôn lấy giá trị cuối cùng từ vòng lặp. Để khắc phục điều này, ta có thể truyền giá trị i
vào hàm Goroutine như sau:
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i <= 100; i++ {
go func(value int) {
fmt.Println(value) // giá trị được truyền vào độc lập
}(i)
}
time.Sleep(time.Second)
}
Giờ đây, mỗi Goroutine sẽ in ra các giá trị khác nhau từ 1 đến 100 mà không bị trùng lặp.
Sử dụng Gosched()
để phân bổ tài nguyên
Khi làm việc với nhiều Goroutines, có thể xảy ra trường hợp một Goroutine chiếm dụng tài nguyên quá mức, khiến cho các Goroutines khác không thể hoạt động. Để giải quyết tình trạng này, bạn có thể sử dụng hàm Gosched()
từ package runtime
để thông báo cho hệ thống phân bổ lại tài nguyên:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
for i := 1; i <= 50; i++ {
fmt.Println("I am Goroutine 1")
runtime.Gosched() // Giao quyền điều khiển cho Goroutine khác
}
}()
go func() {
for i := 1; i <= 50; i++ {
fmt.Println("I am Goroutine 2")
runtime.Gosched() // Giao quyền điều khiển tiếp
}
}()
time.Sleep(time.Second)
}
Sử dụng Gosched()
cho phép ứng dụng hoạt động hiệu quả hơn bằng cách tránh tình trạng chiếm dụng tài nguyên không cần thiết.
Kết luận
Qua bài viết này, chúng ta đã tìm hiểu về Goroutines, đặc điểm và cách sử dụng của chúng trong Golang. Goroutines mang lại nhiều lợi ích cho lập trình viên, giúp họ phát triển các ứng dụng đồng thời một cách hiệu quả và dễ dàng. Hãy tự tin áp dụng các kiến thức này vào công việc và dự án của bạn.
Nếu bạn muốn tìm hiểu sâu hơn về golang và các tính năng của nó, hãy tham khảo các tài liệu và khoá học tại comdy.vn.