Chuyển đến nội dung chính
Mô-đun 7 · Bài học 4 trong tổng số 534/50 trong khóa học~14 min
Bài học theo mô-đun (4/5)

`sync.Mutex` và `sync.WaitGroup`

Kênh là thành ngữ chính của Go, nhưng đôi khi mang tính truyền thống hơn nguyên thủy là cần thiết. Gói sync cung cấp mutexes, hãy đợi các nhóm và các tiện ích khác để phối hợp trạng thái chia sẻ.

sync.Mutex: khóa độc quyền

Go
import "sync"

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

Mỗi lần chỉ một goroutine đi vào giữa Lock() và Unlock(). Mẫu bắt buộc: defer mu.Unlock() ngay sau Lock().

sync.RWMutex: nhiều độc giả, một nhà văn

Đối với trạng thái đọc nặng:

Go
var mu sync.RWMutex

mu.RLock()              // read lock: più goroutine simultanee OK
v := data
mu.RUnlock()

mu.Lock()               // write lock: esclusivo
data = newValue
mu.Unlock()

Chỉ hữu ích nếu số lần đọc thực sự chiếm ưu thế so với số lần ghi; nếu không thì thường xuyên sync.Mutex đơn giản hơn và thường nhanh hơn nhờ chi phí nội bộ thấp hơn sự tranh chấp.

sync.WaitGroup: đợi N goroutines

Go
var wg sync.WaitGroup

for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        work(id)
    }(i)
}
wg.Wait()    // blocca finché tutti i Done() sono arrivati

Quy trình làm việc:

  1. wg.Add(N) tăng bộ đếm lên N (trước khi khởi chạy goroutines).
  2. Mỗi goroutine gọi wg.Done() khi nó kết thúc (lý tưởng nhất là thông qua defer).
  3. wg.Wait() chặn cho đến khi bộ đếm quay về 0.

sync.Once: khởi tạo lười biếng an toàn theo luồng

Go
var (
    once   sync.Once
    config *Config
)

func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig()
    })
    return config
}

Lệnh gọi lại trong once.Do chạy chính xác một lần, ngay cả trong điều kiện đồng thời cuộc gọi. Mẫu khởi tạo Singleton/lazy.

Mutex vs kênh: khi nào nên sử dụng cái gì

Nguyên tắc (cũng từ Câu hỏi thường gặp về Go chính thức):

Trường hợpCông cụ ưa thích
Chuyển quyền sở hữu dữ liệukênh
Phân phối công việc (hàng đợi công việc)kênh
Phối hợp các goroutines độc lậpkênh
Bảo vệ trường cấu trúcTắt tiếng
Bộ nhớ đệm / bộ đếmMutex hoặc nguyên tử
Tài liệu tham khảo Singletonsync.Once

"Các kênh để sắp xếp, các mutex cho dữ liệu được chia sẻ" là một nguyên tắc chung.

Máy dò cuộc đua

Chạy thử nghiệm với -race để phát hiện các cuộc đua dữ liệu:

Bash
go test -race ./...
go run -race main.go

Bộ phát hiện cuộc đua thiết lập hệ nhị phân và ghi lại mọi thông tin chưa được đồng bộ hóa truy cập vào bộ nhớ dùng chung. Đây là công cụ số 1 để xác thực mã đồng thời trước khi sản xuất.

Hãy thử nó

tập thể dục#go.m7.l4.e1
Nỗ lực: 0Đang tải…

Bảo vệ số đếm tăng dần bằng mu.Lock() + defer mu.Unlock().

Đang tải trình chỉnh sửa…
Hiển thị gợi ý

trì hoãn mu.Unlock() ngay sau Lock() đảm bảo giải phóng ngay cả khi hoảng loạn.

Giải pháp khả dụng sau 3 lần thử

tập thể dục#go.m7.l4.e2
Nỗ lực: 0Đang tải…

Khởi chạy 3 goroutine và đợi tất cả chúng sử dụng sync.WaitGroup (Add(3), Done() trong mỗi goroutine, Wait()).

Đang tải trình chỉnh sửa…
Hiển thị gợi ý

Thêm TRƯỚC KHI khởi chạy goroutine; Hoàn thành BÊN TRONG goroutine với defer.

Giải pháp khả dụng sau 3 lần thử

Quiz#go.m7.l4.e3
Sẵn sàng

Mẫu được đề xuất để đảm bảo mutex được phát hành là gì?

Go
mu.Lock()
// ?
Tùy chọn trả lời

Tóm tắt lại

  • sync.Mutex bảo vệ trạng thái chia sẻ; luôn defer mu.Unlock().
  • Sử dụng bộ nhận con trỏ để tránh sao chép mutex.
  • sync.RWMutex dành cho mô hình nhiều người đọc/một người viết (chỉ khi điều đó có ích).
  • sync.WaitGroup: Add trước go, Done với defer, Wait để chờ.
  • sync.Once: khởi tạo lười biếng an toàn theo luồng.
  • "Kênh phối hợp, mutexes cho dữ liệu được chia sẻ".
  • go test -race: công cụ số 1 để phát hiện các cuộc đua dữ liệu.