Go Goroutines và Concurrency Fundamentals

Go Goroutines và Concurrency Fundamentals
Goroutines là một trong những tính năng mạnh mẽ nhất của Go, làm cho concurrent programming trở nên đơn giản và hiệu quả. Trong hướng dẫn toàn diện này, bạn sẽ học cách viết Go programs đồng thời, hiểu runtime scheduler, và xây dựng production-ready applications.
Goroutines Là Gì?
Goroutine là một lightweight thread được quản lý bởi Go runtime. Không giống OS threads, goroutines cực kỳ rẻ để tạo và sử dụng memory tối thiểu.
Đặc điểm chính:
- Lightweight: Bắt đầu chỉ với 2KB stack space (grows as needed)
- Rẻ để tạo: Spawn hàng nghìn hoặc thậm chí hàng triệu goroutines
- Managed bởi Go runtime: Không cần quản lý threads thủ công
- Communicates via channels: Share memory by communicating
Goroutine Đầu Tiên
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from goroutine!")
}
func main() {
// Launch goroutine với 'go' keyword
go sayHello()
// Cho goroutine thời gian execute
time.Sleep(100 * time.Millisecond)
fmt.Println("Main function")
}Output:
Hello from goroutine!
Main functionGoroutines vs Threads
Traditional OS Threads
Memory per thread: ~1-2 MB
Creation cost: Đắt (system call)
Context switching: Chậm (kernel involvement)
Max threads: ~10,000 (depends on OS/memory)Go Goroutines
Memory per goroutine: ~2 KB initial (grows dynamically)
Creation cost: Rất rẻ (user space)
Context switching: Nhanh (Go scheduler)
Max goroutines: Hàng triệu (depends on memory)Ví dụ: Tạo 10,000 Goroutines
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
_ = id * 2
}(i)
}
wg.Wait()
fmt.Println("All 10,000 goroutines completed!")
}Go Scheduler: GMP Model
Components:
- G (Goroutine): Lightweight execution unit
- M (Machine): OS thread
- P (Processor): Scheduling context (logical CPU)
Cách hoạt động:
- Mỗi P có local run queue của goroutines
- M (OS thread) execute goroutines từ P's queue
- Khi goroutine blocks (I/O, syscall), M có thể detached
- Work stealing: Idle P steals goroutines từ busy P's queue
GOMAXPROCS: Controlling Parallelism
package main
import (
"fmt"
"runtime"
)
func main() {
// Get current GOMAXPROCS
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
// Set to 4 processors
runtime.GOMAXPROCS(4)
// Or use all available CPUs (default)
runtime.GOMAXPROCS(runtime.NumCPU())
}Synchronizing Goroutines với WaitGroups
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement counter when done
fmt.Printf("Worker %d: Starting\n", id)
time.Sleep(1 * time.Second)
fmt.Printf("Worker %d: Done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // Increment counter
go worker(i, &wg)
}
wg.Wait() // Block until counter reaches 0
fmt.Println("All workers completed")
}WaitGroup Methods:
Add(delta int): Increment counter by deltaDone(): Decrement counter by 1Wait(): Block until counter reaches 0
Concurrency vs Parallelism
Concurrency: Dealing with multiple things at once (design)
Parallelism: Doing multiple things at once (execution)
CPU-Bound vs I/O-Bound
CPU-Bound Tasks: Heavy computation (limited by CPU)
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}I/O-Bound Tasks: Waiting cho network, disk (limited by I/O)
func fetchURL(url string) {
resp, _ := http.Get(url) // Blocks on network
defer resp.Body.Close()
}Ví dụ: I/O-Bound Concurrency
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
func fetchURL(url string, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body)
fmt.Printf("Fetched %s (%d bytes) in %v\n",
url, len(bytes), time.Since(start))
}
func main() {
urls := []string{
"https://golang.org",
"https://github.com",
"https://stackoverflow.com",
}
var wg sync.WaitGroup
start := time.Now()
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg)
}
wg.Wait()
fmt.Printf("Total time: %v\n", time.Since(start))
}Race Conditions: Kẻ Thù Của Concurrency
Race condition xảy ra khi multiple goroutines truy cập shared data concurrently, và ít nhất một goroutine modify nó.
Example: Counter Race Condition
package main
import (
"fmt"
"sync"
)
var counter int // Shared variable
func increment(wg *sync.WaitGroup) {
defer wg.Done()
counter++ // NOT thread-safe!
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Counter:", counter) // Expected: 1000, Actual: ???
}Detecting Race Conditions
Go có built-in race detector:
go run -race main.go
go test -race ./...
go build -raceExample với Race Detector:
$ go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c000012090 by goroutine 7:
main.increment()
/path/to/main.go:10 +0x3a
==================Avoiding Race Conditions
Solution 1: Mutex (Mutual Exclusion)
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex // Protects counter
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock() // Acquire lock
counter++ // Critical section
mu.Unlock() // Release lock
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Counter:", counter) // Always 1000
}Solution 2: Atomic Operations
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64
func increment(wg *sync.WaitGroup) {
defer wg.Done()
atomic.AddInt64(&counter, 1) // Atomic increment
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Counter:", atomic.LoadInt64(&counter))
}Khi nào dùng:
- Mutex: Protecting complex critical sections
- Atomic: Simple operations (increment, load, store)
Goroutine Leaks: Common Pitfall
Goroutine leak xảy ra khi goroutines được start nhưng không bao giờ terminate.
Fixing Goroutine Leaks với Context
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: Stopping\n", id)
return
default:
// Do work
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 10; i++ {
go worker(ctx, i)
}
time.Sleep(2 * time.Second)
fmt.Println("Canceling workers")
cancel() // Signal all goroutines to stop
time.Sleep(1 * time.Second)
}Real-World Example: Concurrent Web Scraper
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
type Result struct {
URL string
Status int
Size int
Err error
}
func fetch(url string, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
resp, err := http.Get(url)
if err != nil {
results <- Result{URL: url, Err: err}
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
results <- Result{URL: url, Status: resp.StatusCode, Err: err}
return
}
results <- Result{
URL: url,
Status: resp.StatusCode,
Size: len(body),
}
fmt.Printf("Fetched %s in %v\n", url, time.Since(start))
}
func main() {
urls := []string{
"https://golang.org",
"https://github.com",
"https://stackoverflow.com",
"https://reddit.com",
"https://news.ycombinator.com",
}
results := make(chan Result, len(urls))
var wg sync.WaitGroup
// Launch goroutines
for _, url := range urls {
wg.Add(1)
go fetch(url, results, &wg)
}
// Close results channel when all done
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
if result.Err != nil {
fmt.Printf("❌ %s: %v\n", result.URL, result.Err)
} else {
fmt.Printf("✅ %s: %d (%d bytes)\n",
result.URL, result.Status, result.Size)
}
}
}Best Practices cho Goroutines
1. Luôn Biết Khi Goroutines Sẽ Stop
// ❌ BAD: Goroutine có thể leak
go func() {
for {
doWork()
}
}()
// ✅ GOOD: Goroutine có thể stopped
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}(ctx)2. Use WaitGroups cho Synchronization
// ❌ BAD: Using sleep
go worker()
time.Sleep(1 * time.Second) // Không reliable!
// ✅ GOOD: Using WaitGroup
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
worker()
}()
wg.Wait() // Reliable3. Avoid Capturing Loop Variables
// ❌ BAD: Race condition
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // Tất cả goroutines thấy cùng 'i'
}()
}
// ✅ GOOD: Pass as argument
for i := 0; i < 10; i++ {
go func(id int) {
fmt.Println(id)
}(i)
}4. Luôn Run Tests với Race Detector
go test -race ./...Performance Considerations
Goroutine Creation Overhead
Tạo goroutines rẻ nhưng không free:
func BenchmarkGoroutineCreation(b *testing.B) {
for i := 0; i < b.N; i++ {
go func() {}()
}
}
// Result: ~1-2 microseconds per goroutineMemory Usage
Mỗi goroutine dùng ~2KB stack initially:
1,000 goroutines = ~2 MB
10,000 goroutines = ~20 MB
100,000 goroutines = ~200 MB
1,000,000 goroutines = ~2 GBMonitor với:
fmt.Println("Goroutines:", runtime.NumGoroutine())Common Goroutine Patterns
Pattern 1: Fire-and-Forget
go logToFile(data) // Không đợi completionPattern 2: Fan-Out (Distribute Work)
for _, task := range tasks {
go processTask(task)
}Pattern 3: Fan-In (Collect Results)
results := make(chan Result)
for _, url := range urls {
go fetch(url, results)
}Pattern 4: Worker Pool
const numWorkers = 5
jobs := make(chan Job, 100)
for i := 0; i < numWorkers; i++ {
go worker(jobs)
}Tóm Tắt và Key Takeaways
✅ Goroutines là lightweight threads managed bởi Go runtime
✅ Dùng go keyword để launch goroutines
✅ WaitGroups synchronize goroutine completion
✅ Race conditions xảy ra khi goroutines access shared data unsafe
✅ Dùng -race flag để detect race conditions
✅ Mutexes và atomic operations protect shared state
✅ GOMAXPROCS controls parallelism (default = NumCPU)
✅ Concurrency ≠ Parallelism: Concurrency là design, parallelism là execution
✅ Goroutine leaks xảy ra khi goroutines không bao giờ terminate
✅ Dùng context.Context để cancel goroutines gracefully
✅ Monitor goroutine count với runtime.NumGoroutine()
Tiếp Theo Là Gì?
Bây giờ bạn đã hiểu goroutines, bạn đã sẵn sàng học Channels - cách Go communicate giữa các goroutines:
Bài tiếp theo: Channels and Communication (GO-9)
Chủ đề:
- Channel basics (buffered vs unbuffered)
- Select statement cho multiplexing
- Channel patterns (fan-out, fan-in, pipelines)
- Worker pools
- Context cho cancellation
- Khi nào dùng channels vs mutexes
Tài Nguyên Bổ Sung
Tài liệu chính thức:
Deep Dives:
Sách:
- "Concurrency in Go" by Katherine Cox-Buday
- "Go in Action" by William Kennedy
Có câu hỏi hoặc feedback? Để lại comment bên dưới!
Chúc concurrent programming vui vẻ! 🚀
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.