Đa luồng trong ngôn ngữ lập trình go
Đa luồng (multithreading) là một kỹ thuật cho phép nhiều luồng (thread) có thể chạy song song trên một bộ vi xử lý (CPU) hoặc nhiều bộ vi xử lý. Đa luồng giúp tăng hiệu suất và khai thác tối đa nền tảng đa lõi của CPU. Tuy nhiên, đa luồng cũng gặp phải nhiều vấn đề như context switch, cache coherence, synchronization, deadlock, race condition, …
Go là một ngôn ngữ lập trình mã nguồn mở, dạng biên dịch, có kiểu tĩnh. Go được thiết kế để chạy đa luồng và hỗ trợ concurrency rất tốt². Concurrency là khả năng xử lý nhiều việc cùng một lúc bằng cách chia nhỏ thành các đơn vị nhỏ hơn và phân phối cho các luồng. Go sử dụng một khái niệm gọi là goroutine để thực hiện concurrency.
Goroutine là một hàm hoặc một khối lệnh được thực thi bởi một luồng ảo (virtual thread) của Go runtime. Goroutine có thể được coi như là một luồng nhẹ (lightweight thread), vì nó không cần phải được quản lý bởi hệ điều hành và có thể chạy trên một luồng vật lý (physical thread) của CPU. Goroutine có dung lượng bộ nhớ nhỏ (khoảng 2KB), có thể được khởi tạo nhanh chóng và có thể chuyển đổi giữa các luồng vật lý một cách linh hoạt.
Để khởi tạo một goroutine, chỉ cần thêm từ khóa go trước khi gọi hàm hoặc khối lệnh. Ví dụ:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello")
}
func main() {
go sayHello() // Khởi tạo một goroutine để chạy hàm sayHello
time.Sleep(1 * time.Second) // Dừng chương trình chính trong 1 giây
fmt.Println("Bye")
}
Kết quả:
Hello
Bye
Trong ví dụ trên, chương trình chính sẽ khởi tạo một goroutine để chạy hàm sayHello, sau đó tiếp tục thực thi các lệnh tiếp theo. Goroutine sẽ chạy song song với chương trình chính và in ra “Hello”. Chương trình chính sẽ dừng lại trong 1 giây để đợi goroutine hoàn thành, sau đó in ra “Bye” và kết thúc.
Goroutine có thể giao tiếp với nhau bằng cách sử dụng các kênh (channel). Channel là một cấu trúc dữ liệu cho phép truyền nhận các giá trị giữa các goroutine theo cơ chế FIFO (first in first out). Channel có thể được tạo ra bằng từ khóa make và có thể được chỉ định kiểu dữ liệu của các giá trị được truyền nhận. Ví dụ:
package main
import (
"fmt"
)
func sum(a []int, c chan int) {
sum := 0
for _, v := range a {
sum += v
}
c <- sum // Gửi tổng của mảng a vào kênh c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int) // Tạo một kênh c có kiểu dữ liệu là int
go sum(a[:len(a)/2], c) // Khởi tạo một goroutine để tính tổng nửa đầu của mảng a và gửi vào kênh c
go sum(a[len(a)/2:], c) // Khởi tạo một goroutine để tính tổng nửa sau của mảng a và gửi vào kênh c
x, y := <-c, <-c // Nhận hai giá trị từ kênh c và gán cho x và y
fmt.Println(x, y, x+y)
}
Kết quả:
-5 17 12
Trong ví dụ trên, chương trình chính sẽ tạo một kênh c có kiểu dữ liệu là int, sau đó khởi tạo hai goroutine để tính tổng hai nửa của mảng a và gửi vào kênh c. Chương trình chính sẽ nhận hai giá trị từ kênh c và gán cho x và y, sau đó in ra kết quả. Lưu ý rằng việc gửi và nhận dữ liệu qua kênh là đồng bộ (synchronous), nghĩa là nếu không có goroutine nào gửi hoặc nhận dữ liệu thì chương trình sẽ bị khóa (block).
Go còn có nhiều tính năng khác hỗ trợ cho đa luồng và concurrency như select, defer, panic, recover, … Bạn có thể tìm hiểu thêm tại trang chủ hoặc tài liệu của Go.
Hy vọng bản nháp này có ích cho bạn. Nếu bạn muốn tôi sửa đổi hoặc bổ sung gì, hãy cho tôi biết.
Source:
(1) Go (ngôn ngữ lập trình) – Wikipedia tiếng Việt. https://vi.wikipedia.org/wiki/Go_%28ng%C3%B4n_ng%E1%BB%AF_l%E1%BA%ADp_tr%C3%ACnh%29.
(2) Ngôn ngữ lập trình Go và Goroutines là gì mà dân tình xôn xao thế?. https://bing.com/search?q=%c4%91a+lu%e1%bb%93ng+trong+ng%c3%b4n+ng%e1%bb%af+l%e1%ba%adp+tr%c3%acnh+go.
(3) Đa luồng nhanh hay chậm? – CodeLearn. https://codelearn.io/sharing/da-luong-nhanh-hay-cham.
(4) Ngôn ngữ lập trình Go và Goroutines là gì mà dân tình xôn xao thế?. https://news.sun-asterisk.com/p/ngon-ngu-lap-trinh-go-va-goroutines-la-gi-ma-dan-tinh-xon-xao-the-JnbmMRV2DBz.