Trong các chương trình Go sử dụng goroutines, việc sử dụng biến toàn cục (global variables) thường không được khuyến khích và có thể dẫn đến các vấn đề liên quan đến đồng bộ hóa và tính an toàn của dữ liệu. Dưới đây là lý do tại sao bạn nên cẩn thận khi sử dụng biến toàn cục trong các chương trình sử dụng goroutines:

1. Trạng thái chia sẻ không an toàn giữa các goroutines

Goroutines thực hiện các tác vụ song song và có thể truy cập, thay đổi các biến toàn cục cùng một lúc. Nếu không có sự đồng bộ hóa hợp lý, điều này có thể gây ra các vấn đề về race condition (điều kiện tranh chấp), trong đó nhiều goroutine có thể truy cập và thay đổi biến toàn cục đồng thời, dẫn đến kết quả không xác định hoặc lỗi chương trình.

Ví dụ về race condition khi sử dụng biến toàn cục:

package main

import (
    "fmt"
    "time"
)

var counter int // Biến toàn cục

func increment() {
    for i := 0; i < 1000; i++ {
        counter++
    }
}

func main() {
    go increment()
    go increment()

    time.Sleep(time.Second) // Đợi goroutines chạy xong
    fmt.Println("Giá trị của counter:", counter)
}

Trong ví dụ này, kết quả của counter có thể không phải là 2000 như mong đợi, do cả hai goroutines đang cố gắng tăng giá trị của counter cùng lúc, gây ra race condition.

2. Race condition và công cụ go run -race

Go cung cấp một công cụ giúp phát hiện các điều kiện race condition. Bạn có thể chạy chương trình Go với flag -race để kiểm tra liệu có xảy ra race condition hay không:

go run -race main.go

Nếu phát hiện race condition, công cụ này sẽ in ra cảnh báo kèm theo thông tin về vị trí vấn đề xảy ra.

3. Cách khắc phục

Để tránh các vấn đề liên quan đến biến toàn cục khi sử dụng goroutines, bạn nên sử dụng các cơ chế đồng bộ hóa hoặc truyền dữ liệu an toàn qua các kênh (channels).

a. Sử dụng sync.Mutex

Một cách để đảm bảo rằng chỉ một goroutine có thể truy cập và thay đổi biến toàn cục tại một thời điểm là sử dụng mutex từ gói sync.

Ví dụ sử dụng sync.Mutex:

package main

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

var counter int // Biến toàn cục
var mutex sync.Mutex

func increment() {
    for i := 0; i < 1000; i++ {
        mutex.Lock()   // Đảm bảo chỉ có một goroutine truy cập biến counter
        counter++
        mutex.Unlock() // Mở khóa để các goroutine khác có thể truy cập
    }
}

func main() {
    go increment()
    go increment()

    time.Sleep(time.Second) // Đợi goroutines chạy xong
    fmt.Println("Giá trị của counter:", counter)
}

Trong ví dụ này, mutex.Lock() đảm bảo rằng chỉ một goroutine có quyền truy cập vào biến counter tại một thời điểm, ngăn chặn race condition.

b. Sử dụng kênh (channels)

Một cách khác để tránh sử dụng biến toàn cục và đồng bộ hóa là sử dụng channels để truyền dữ liệu giữa các goroutines. Channels giúp đảm bảo tính an toàn của dữ liệu mà không cần dùng đến biến toàn cục.

Ví dụ sử dụng channels:

package main

import (
    "fmt"
    "time"
)

func increment(ch chan int) {
    counter := 0
    for i := 0; i < 1000; i++ {
        counter++
    }
    ch <- counter // Gửi kết quả qua kênh
}

func main() {
    ch := make(chan int)

    go increment(ch)
    go increment(ch)

    counter1 := <-ch
    counter2 := <-ch

    total := counter1 + counter2
    fmt.Println("Tổng giá trị của counter:", total)
}

Trong ví dụ này, dữ liệu được truyền qua kênh giữa các goroutines, loại bỏ nhu cầu sử dụng biến toàn cục và đảm bảo tính an toàn của dữ liệu.

4. Tóm tắt

  • Không nên sử dụng biến toàn cục trong các chương trình triển khai goroutines, vì nó có thể dẫn đến các vấn đề như race condition, gây lỗi khó lường.
  • Nếu bạn bắt buộc phải sử dụng biến toàn cục, cần sử dụng các cơ chế đồng bộ hóa như sync.Mutex hoặc sync.RWMutex để đảm bảo rằng chỉ một goroutine có thể truy cập biến tại một thời điểm.
  • Channels là một giải pháp an toàn và hiệu quả trong Go để truyền dữ liệu giữa các goroutines mà không cần sử dụng biến toàn cục.

Sử dụng các giải pháp này sẽ giúp đảm bảo tính an toàn, rõ ràng, và dễ bảo trì cho chương trình của bạn khi làm việc với goroutines.