Sử dụng Goroutine trong Go

Tổng quan về Goroutine trong Go

Các khái niệm cơ bản

Trước khi tìm hiểu sâu về Goroutine, chúng ta cần làm rõ một số thuật ngữ:

  • Quá trình (Process): Là lần thực thi của một chương trình trên hệ điều hành. Mỗi lần chạy chương trình, hệ điều hành sẽ gán cho nó một ID và tài nguyên độc lập.
  • Làn (Thread): Là một thực thể thuộc quá trình, là đơn vị nhỏ nhất mà CPU có thể phân bổ và thực hiện.

Về bình song (Parallel)cùng lúc (Concurrency):

  • Bình song: Nhiều công việc được thực hiện đồng thời trên nhiều CPU khác nhau.
  • Cùng lúc: Các tác vụ luân phiên sử dụng tài nguyên CPU để tạo ảo giác rằng chúng đang hoạt động đồng thời.

Goroutine trong Go

Go cung cấp tính năng hỗ trợ cùng lúc thông qua Goroutine, một dạng thread nhẹ hơn rất nhiều so với thread tiêu chuẩn. Goroutine do runtime của Go quản lý và tự động tối ưu hóa việc sử dụng các nguồn lực hệ thống.

Tại sao chọn Goroutine?

  • Goroutine chiếm ít tài nguyên hơn so với thread tiêu chuẩn (chỉ khoảng 2KB ban đầu).
  • Goroutine được chuyển đổi nhanh chóng bởi scheduler bên trong runtime của Go thay vì phụ thuộc vào hệ điều hành.

Mối liên hệ giữa Goroutine và Thread

Goroutine không phải là thread thuần túy mà là một lớp trừu tượng cao hơn. Runtime của Go sẽ tự động phân phối các Goroutine lên các thread hợp lý dựa trên số lượng CPU và tải hệ thống.

Thực hành với Goroutine

Tạo Goroutine

Để khởi tạo một Goroutine, bạn chỉ cần thêm từ khóa go trước tên hàm:

go funcName()

Ví dụ minh họa

package main

import (
    "fmt"
    "time"
)

func main() {
    go performTask()
    fmt.Println("Chương trình chính tiếp tục...")
}

func performTask() {
    for i := 0; i < 5; i++ {
        fmt.Printf("Goroutine đang chạy... %d\n", i)
        time.Sleep(1 * time.Second)
    }
}

Khi chạy đoạn mã trên, bạn sẽ thấy chương trình chính không chờ Goroutine hoàn thành mà tiếp tục thực hiện ngay lập tức.

Cải thiện hiệu suất bằng cách sử dụng Goroutine

Hãy tưởng tượng bạn cần lấy dữ liệu từ nhiều nguồn khác nhau. Thay vì thực hiện tuần tự, hãy dùng Goroutine để tăng tốc độ xử lý.

Không dùng Goroutine

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 1; i <= 5; i++ {
        fmt.Println(processData(i))
    }
}

func processData(num int) int {
    time.Sleep(2 * time.Second)
    return num * num
}

Trong ví dụ này, mỗi lần gọi hàm processData đều mất 2 giây, dẫn đến tổng thời gian xử lý là 10 giây.

Sử dụng Goroutine

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func main() {
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            result := processData(index)
            fmt.Printf("Kết quả từ task %d: %d\n", index, result)
        }(i)
    }

    wg.Wait()
}

func processData(num int) int {
    time.Sleep(2 * time.Second)
    return num * num
}

Đoạn mã này sẽ hoàn thành sau khoảng 2 giây nhờ việc chạy đồng thời các tác vụ.

Xử lý vấn đề Goroutine chưa hoàn thành

Nếu chương trình chính kết thúc trước khi tất cả Goroutine hoàn thành, các Goroutine còn lại sẽ bị hủy bỏ. Để giải quyết vấn đề này, chúng ta có thể sử dụng sync.WaitGroup.

Ví dụ với WaitGroup

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            result := calculate(index)
            fmt.Printf("Tính toán xong phần tử %d: %d\n", index, result)
        }(i)
    }

    wg.Wait()
}

func calculate(num int) int {
    return num * num
}

WaitGroup giúp chương trình chính đợi cho đến khi tất cả các Goroutine đã hoàn thành.

Các vấn đề về cùng lúc trong Go

Một vấn đề phổ biến khi sử dụng Goroutine là tình trạng tranh chấp tài nguyên. Ví dụ:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    var shared []int
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            shared = append(shared, process(index))
        }(i)
    }

    wg.Wait()
    fmt.Println("Danh sách kết quả:", shared)
}

func process(num int) int {
    return num * num
}

Kết quả có thể không đúng như mong muốn do nhiều Goroutine truy cập và thay đổi cùng một tài nguyên (shared) mà không kiểm soát.

Giải pháp

Thay vì chỉnh sửa trực tiếp vào biến chia sẻ, chúng ta có thể sử dụng channel để truyền dữ liệu giữa các Goroutine:

package main

import (
    "fmt"
)

func main() {
    results := make(chan int)
    for i := 1; i <= 5; i++ {
        go func(index int) {
            results <- process(index)
        }(i)
    }

    close(results)
    finalResults := []int{}
    for res := range results {
        finalResults = append(finalResults, res)
    }

    fmt.Println("Danh sách kết quả:", finalResults)
}

func process(num int) int {
    return num * num
}

Với cách tiếp cận này, dữ liệu được gửi qua channel và xử lý sau khi tất cả Goroutine hoàn thành.

Thẻ: Go Goroutine Concurrency

Đăng vào ngày 16 tháng 6 lúc 07:26