Back to blog

Go Goroutines và Concurrency Fundamentals

gogolangconcurrencygoroutinesbackend
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 function

Goroutines 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:

  1. Mỗi P có local run queue của goroutines
  2. M (OS thread) execute goroutines từ P's queue
  3. Khi goroutine blocks (I/O, syscall), M có thể detached
  4. 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 delta
  • Done(): Decrement counter by 1
  • Wait(): 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 -race

Example 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() // Reliable

3. 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 goroutine

Memory 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 GB

Monitor với:

fmt.Println("Goroutines:", runtime.NumGoroutine())

Common Goroutine Patterns

Pattern 1: Fire-and-Forget

go logToFile(data) // Không đợi completion

Pattern 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.