Goroutine
Goroutines in Go - Comprehensive Guide
1. Goroutines là gì?
Goroutine là một đơn vị thực thi nhẹ (lightweight thread) được quản lý bởi runtime của Go, không phải hệ điều hành. Goroutines cho phép thực hiện đồng thời (concurrency) nhiều tác vụ trong một chương trình.
Đặc điểm:
Nhẹ: Một goroutine chỉ chiếm vài KB bộ nhớ (so với thread hệ điều hành chiếm MB).
Quản lý bởi Go runtime: Go scheduler phân bổ goroutines trên các thread hệ điều hành (OS threads) để tối ưu hóa hiệu năng.
Khởi tạo đơn giản: Chỉ cần từ khóa
go
trước một hàm.
Goroutines là nền tảng của mô hình concurrency trong Go, kết hợp với channels để đồng bộ và giao tiếp.
Ví dụ cơ bản:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from goroutine")
}
func main() {
go sayHello() // Chạy sayHello trong goroutine
time.Sleep(time.Second) // Đợi để goroutine hoàn thành
fmt.Println("Main done")
}
Lưu ý:
time.Sleep
chỉ để minh họa. Trong thực tế, nên dùng cơ chế đồng bộ như channels hoặcsync.WaitGroup
.
2. Cách hoạt động của Goroutines
Go Scheduler:
Go runtime có một scheduler (dựa trên mô hình M:N) ánh xạ nhiều goroutines (M) lên ít thread hệ điều hành (N).
Scheduler sử dụng cơ chế preemptive scheduling (từ Go 1.14) để ngăn goroutines chiếm CPU quá lâu.
Các điểm tạm dừng (yield points) bao gồm: gọi hàm, I/O, channel operations, hoặc garbage collection.
Stack: Mỗi goroutine bắt đầu với stack nhỏ (~2KB) và tự động mở rộng nếu cần (lên đến 1GB).
Ví dụ minh họa scheduler:
package main
import (
"fmt"
"runtime"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Printf("Goroutine %d: %d\n", runtime.NumGoroutine(), i)
runtime.Gosched() // Nhường CPU (chỉ để minh họa)
}
}
func main() {
for i := 0; i < 3; i++ {
go printNumbers()
}
time.Sleep(time.Second)
}
Kết quả: Các goroutine chạy xen kẽ do scheduler điều phối.
3. Khởi tạo và quản lý Goroutines
3.1. Khởi tạo Goroutine
Sử dụng từ khóa
go
trước một hàm hoặc closure:
go func() {
fmt.Println("Anonymous goroutine")
}()
Lưu ý: Goroutine chạy ngay lập tức nhưng không đảm bảo thứ tự hoàn thành.
3.2. Đồng bộ Goroutines
Goroutines không tự đợi nhau. Để đồng bộ, sử dụng:
a. sync.WaitGroup
Dùng để đợi một nhóm goroutines hoàn thành.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
Mẹo:
Gọi
wg.Add(1)
trướcgo
để tránh race condition.Truyền
&wg
để chia sẻWaitGroup
.
b. Channels
Channels là cách chính để giao tiếp và đồng bộ giữa goroutines (xem chi tiết ở phần 5).
4. Goroutines và Concurrency Patterns
4.1. Worker Pool
Tạo một nhóm goroutines cố định để xử lý công việc từ một kênh.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
results <- job * 2
}
}
func main() {
const numWorkers = 3
const numJobs = 10
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Khởi động worker pool
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
// Gửi công việc
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Đợi và thu thập kết quả
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Println("Result:", result)
}
}
Ưu điểm: Giới hạn số lượng goroutines, tránh tiêu tốn tài nguyên.
4.2. Fan-out/Fan-in
Phân chia công việc cho nhiều goroutines (fan-out) và thu thập kết quả (fan-in).
package main
import (
"fmt"
"sync"
)
func producer(id int, out chan<- int) {
for i := 0; i < 3; i++ {
out <- id*10 + i
}
}
func consumer(in <-chan int, done chan<- struct{}) {
for n := range in {
fmt.Println("Consumed:", n)
}
done <- struct{}{}
}
func main() {
out := make(chan int)
done := make(chan struct{})
var wg sync.WaitGroup
// Fan-out: 3 producer
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
producer(id, out)
}(i)
}
// Fan-in: 1 consumer
go consumer(out, done)
// Đóng out khi tất cả producer xong
go func() {
wg.Wait()
close(out)
}()
<-done // Đợi consumer xong
}
Ứng dụng: Xử lý song song các tác vụ độc lập (như gọi API, xử lý file).
4.3. Pipeline
Xây dựng một chuỗi xử lý dữ liệu qua nhiều giai đoạn.
package main
import "fmt"
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
in := gen(2, 3, 4)
out := square(in)
for n := range out {
fmt.Println(n) // Output: 4, 9, 16
}
}
Ưu điểm: Tăng tính tái sử dụng và dễ mở rộng.
5. Channels và Goroutines
Channels là cơ chế chính để giao tiếp giữa goroutines, đảm bảo an toàn (thread-safe).
Các loại channel:
Unbuffered: Đồng bộ, gửi và nhận phải xảy ra cùng lúc.
Buffered: Không đồng bộ, có thể chứa một số lượng giá trị.
Ví dụ unbuffered channel:
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello"
}()
fmt.Println(<-ch) // Output: Hello
}
Ví dụ buffered channel:
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch) // Output: 1
fmt.Println(<-ch) // Output: 2
}
Mẹo:
Đóng channel (
close(ch)
) khi không còn dữ liệu để gửi.Kiểm tra channel đóng bằng cú pháp:
v, ok := <-ch
.
6. Error Handling trong Goroutines
Goroutines chạy độc lập, nên lỗi trong một goroutine không tự động dừng chương trình.
Cách xử lý lỗi:
Truyền lỗi qua channel.
Sử dụng
panic
vàrecover
(hiếm dùng).
Ví dụ xử lý lỗi:
package main
import (
"errors"
"fmt"
)
func worker(id int, results chan<- error) {
if id%2 == 0 {
results <- errors.New("worker failed")
} else {
results <- nil
}
}
func main() {
results := make(chan error, 3)
for i := 1; i <= 3; i++ {
go worker(i, results)
}
for i := 1; i <= 3; i++ {
if err := <-results; err != nil {
fmt.Println("Error:", err)
}
}
}
Mẹo: Dùng channel để thu thập lỗi từ nhiều goroutines.
7. Tối ưu hóa Goroutines
7.1. Giới hạn số lượng Goroutines
Tạo quá nhiều goroutines có thể tiêu tốn bộ nhớ và CPU.
Sử dụng worker pool hoặc semaphore để giới hạn.
Ví dụ semaphore:
package main
import (
"fmt"
"sync"
"golang.org/x/sync/semaphore"
)
func main() {
var (
maxWorkers = 2
sem = semaphore.NewWeighted(int64(maxWorkers))
wg sync.WaitGroup
)
for i := 1; i <= 5; i++ {
wg.Add(1)
sem.Acquire(context.Background(), 1)
go func(id int) {
defer wg.Done()
defer sem.Release(1)
fmt.Printf("Worker %d working\n", id)
time.Sleep(time.Second)
}(i)
}
wg.Wait()
}
7.2. Tránh Goroutine Leak
Goroutine leak xảy ra khi goroutine không bao giờ kết thúc, gây rò rỉ tài nguyên.
Ví dụ leak:
func leaky() chan int {
ch := make(chan int)
go func() {
<-ch // Chờ mãi nếu không ai gửi
}()
return ch
}
Cách sửa:
func safe(ctx context.Context) chan int {
ch := make(chan int)
go func() {
select {
case <-ch:
case <-ctx.Done():
}
}()
return ch
}
Mẹo: Sử dụng
context
để hủy goroutines khi không cần.
7.3. Tối ưu hiệu năng
GOMAXPROCS: Điều chỉnh số thread hệ điều hành bằng
runtime.GOMAXPROCS(n)
. Mặc định bằng số CPU core.Profile: Dùng
pprof
để tìm bottleneck trong concurrency.Buffered vs Unbuffered: Chọn loại channel phù hợp để giảm blocking.
8. Những lỗi thường gặp
Data Race:
Nhiều goroutines truy cập cùng biến mà không có đồng bộ.
Sửa: Dùng mutex hoặc channel.
Debug: Chạy với
-race
(go run -race
).
Goroutine Leak:
Không đóng channel hoặc goroutine chờ mãi.
Sửa: Dùng
context
hoặc đảm bảo channel được đóng.
Deadlock:
Các goroutines chờ nhau mãi.
Sửa: Kiểm tra logic channel và dùng
select
với default case.
Sử dụng
time.Sleep
để đồng bộ:Không đáng tin cậy.
Sửa: Dùng
WaitGroup
hoặc channel.
9. Ứng dụng thực tế
API Server: Gọi nhiều API song song bằng goroutines.
Data Processing: Xử lý file lớn hoặc dữ liệu streaming.
Web Crawler: Thu thập dữ liệu từ nhiều URL đồng thời.
Real-time Systems: Xử lý websocket, chat, hoặc monitoring.
Ví dụ API song song:
package main
import (
"fmt"
"sync"
"time"
)
func fetchAPI(id int) string {
time.Sleep(time.Millisecond * 100)
return fmt.Sprintf("Result %d", id)
}
func main() {
var wg sync.WaitGroup
results := make([]string, 5)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
results[id] = fetchAPI(id)
}(i)
}
wg.Wait()
fmt.Println(results)
}
11. Tài liệu tham khảo
Công cụ:
go tool pprof
,go run -race
.
Last updated