Bài 34 cho thấy lỗi đua khó phát hiện tới mức nào: ba lần chạy, ba kết quả, và có lần ra đúng. Go có công cụ giải quyết đúng vấn đề đó.
Bật
go run -race main.go
go test -race ./...
go build -race
Và nó chỉ thẳng ra chỗ hỏng:
WARNING: DATA RACE
Read at 0x00c000180028 by goroutine 16:
main.main.func1()
/g/b33/main.go:67 +0x94
Previous write at 0x00c000180028 by goroutine 9:
main.main.func1()
/g/b33/main.go:67 +0xa4
Địa chỉ bộ nhớ, hai goroutine liên quan, và số dòng của cả lần đọc lẫn lần ghi.
Đây là thứ mà bài 96 sê-ri Java phải dựng cả một bộ khung mới có được — trong Go nó là một cờ dòng lệnh.
Chi phí
không -race : 163 ms | binary 2,1 MB
có -race : 972 ms | binary 2,7 MB
Chậm khoảng sáu lần, binary lớn hơn 30%. Tài liệu chính thức nói chậm 2–20 lần và tốn 5–10 lần bộ nhớ, tuỳ tải.
Nên: đừng bật trong sản xuất, nhưng luôn bật trong test và CI.
Nó hoạt động ra sao
Race detector không phân tích tĩnh — nó theo dõi lúc chạy. Với mỗi truy cập bộ nhớ, nó ghi lại goroutine nào đọc, goroutine nào ghi, và quan hệ xảy-ra-trước giữa chúng.
Khi phát hiện hai truy cập tới cùng địa chỉ mà ít nhất một là ghi, và không có quan hệ đồng bộ giữa chúng, nó báo.
Hệ quả quan trọng nhất từ cách hoạt động này:
Đây là lý do phải chạy -race với bộ test có độ phủ tốt, và tốt nhất là dưới tải giống thật.
Những gì nó không bắt
Lỗi logic đồng thời không phải đua bộ nhớ. Deadlock, goroutine rò rỉ, thứ tự sai — nó không quan tâm.
Đua qua hệ thống bên ngoài — hai tiến trình cùng ghi một tệp, hai bản sao dịch vụ cùng cập nhật một dòng CSDL.
Đường mã hiếm. Nhánh lỗi, xử lý hết giờ, mã chỉ chạy khi hệ thống quá tải.
Nó cũng không có dương tính giả: mọi cảnh báo nó đưa ra đều là lỗi đua thật. Nên đừng bao giờ bỏ qua một cảnh báo vì nghĩ "chắc nó nhầm".
Đưa vào CI
- run: go test -race ./...
Một dòng, và nó bắt được cả một lớp lỗi mà test thường bỏ sót.
Nếu bộ test chạy quá lâu với -race, hãy tách: test nhanh chạy mọi lần push, test có -race chạy trên nhánh chính hoặc theo lịch. Nhưng đừng bỏ hẳn.
Một mẹo: -race kết hợp với -count=N chạy lại test nhiều lần, tăng cơ hội trúng đường mã hiếm:
go test -race -count=10 ./...
Sửa lỗi đua
Khi nhận được cảnh báo, có bốn cách theo thứ tự nên thử:
Không chia sẻ. Mỗi goroutine có bản sao riêng, gộp kết quả ở cuối. Đây là cách tốt nhất khi làm được.
Chuyển giao quyền sở hữu qua channel. Chỉ một goroutine chạm vào dữ liệu tại một thời điểm.
Khoá — Mutex như bài 34.
atomic cho thao tác đơn giản trên một biến.
Điều không nên làm: thêm time.Sleep để "tránh" đua. Nó chỉ làm lỗi hiếm hơn, và biến một lỗi tái hiện được thành một lỗi không tài nào tái hiện.
Cái bẫy phổ biến nhất: biến bị bắt trong closure
for _, v := range ds {
go func() { xuLy(v) }() // trước Go 1.22: đua trên v
}
Bài 6 đã đo: Go 1.22 sửa ngữ nghĩa biến vòng lặp, nên đoạn này giờ đúng. Nhưng nếu go.mod của bạn khai dưới go 1.22, nó vẫn là lỗi đua — và race detector sẽ bắt được.
Đây là ví dụ tốt cho thấy vì sao nên kiểm dòng go trong go.mod.
Thử ba mươi giây
go test -race ./... 2>&1 | grep -c "DATA RACE"
Nếu con số lớn hơn 0, bạn vừa tìm ra những lỗi mà bộ test hiện tại đang chạy xanh bên cạnh. Và như bài 34 đã đo, chúng có thể đã sai kết quả trong sản xuất suốt thời gian qua mà không ai biết.
Ngày mai: ba mẫu đồng thời dùng nhiều nhất — fan-out, fan-in và pipeline.