Bài cuối của chặng đồng thời, và nó về câu hỏi bị trả lời sai nhiều nhất trong cộng đồng Go.

Châm ngôn

Don't communicate by sharing memory; share memory by communicating.

Câu này in trên áo, dán trên tường, và bị hiểu thành "luôn dùng channel, đừng dùng khoá".

Chính Rob Pike đã nói rõ đó không phải ý ông: câu châm ngôn nói về cách nghĩ, không phải một lệnh cấm. Và thư viện chuẩn của Go dùng sync.Mutex ở khắp nơi.

Số đo

Tám goroutine, 500.000 thao tác tăng bộ đếm:

  Mutex   : 21 ms
  atomic  :  6 ms
  channel : 12 ms

Channel nhanh hơn Mutex ở phép đo này — đi ngược niềm tin phổ biến.

Nhưng đừng vội kết luận. Lý do channel thắng nằm ở cấu trúc, không ở công cụ: channel có đệm 1000 nên tám goroutine gửi hiếm khi phải chặn, và chỉ một goroutine duy nhất đếm. Không có tranh chấp.

Mutex thì bị cả tám goroutine tranh nhau ở mỗi thao tác.

Nếu tôi đổi thiết kế — mỗi goroutine giữ bộ đếm riêng rồi cộng lại ở cuối — cả hai đều bị atomic bỏ xa, và thậm chí một biến thường cũng thắng.

Bài học không phải "channel nhanh hơn". Nó là: con số phụ thuộc vào cách bạn cấu trúc bài toán nhiều hơn phụ thuộc vào công cụ. So sánh "channel với mutex" tách khỏi ngữ cảnh là một câu hỏi không có câu trả lời.

Bộ tiêu chí chọn

Thay vì hỏi cái nào nhanh hơn, hãy hỏi bài toán của bạn thuộc dạng nào.

Dùng atomic khi: một biến duy nhất, thao tác đơn giản (đếm, cờ, thay con trỏ). Nhanh nhất, và không có gì để dùng sai ngoài việc quên rằng nó chỉ bảo vệ một biến.

Dùng Mutex khi: bảo vệ trạng thái — một struct với vài trường phải nhất quán, một map, một cache. Đây là trường hợp phổ biến nhất trong mã ứng dụng, và mã ngắn hơn channel rất nhiều.

Dùng channel khi: chuyển giao quyền sở hữu dữ liệu giữa các giai đoạn, phối hợp thời điểm, hoặc khi có luồng dữ liệu đi qua nhiều bước. Pipeline ở bài 37 là ví dụ mẫu.

Dùng errgroup khi: chạy nhiều việc độc lập và cần biết cái nào hỏng.

Không dùng gì cả khi: chia dữ liệu để mỗi goroutine có phần riêng, gộp kết quả ở cuối. Đây là phương án nhanh nhất và cũng đúng nhất — và thường bị bỏ qua vì nó không dùng công cụ đồng thời nào.

Dấu hiệu bạn đang chọn sai

Dùng channel để bảo vệ một biến:

type Dem struct{ ch chan int }        // phức tạp không cần thiết

Nếu goroutine duy nhất của bạn chỉ ngồi đọc channel rồi tăng một biến, atomic làm điều đó trong một dòng.

Dùng Mutex để điều phối thứ tự:

mu.Lock()
// chờ bước trước xong...

Khoá không diễn đạt được "chờ tới khi". Đó là việc của channel hoặc sync.Cond.

Khoá giữ quá lâu:

mu.Lock()
resp, _ := http.Get(url)     // GỌI MẠNG trong khi giữ khoá
mu.Unlock()

Đây là lỗi nghiêm trọng nhất, và bài 71 sê-ri Java đã đo: đưa việc chậm ra ngoài khoá giảm thời gian chạy năm lần. Nguyên tắc giống hệt trong Go.

Ba nguyên tắc rút từ cả chặng

Thiết kế để không chia sẻ. Mọi công cụ trong bài này đều là cách xử lý hậu quả của việc chia sẻ trạng thái. Không chia sẻ thì không cần công cụ nào.

Đo, đừng tin châm ngôn. Bảng ở đầu bài lật ngược một niềm tin rất phổ biến — và nó vẫn chỉ đúng cho đúng cấu trúc đó.

Bật -race trong CI. Bài 36 đã đo: chậm sáu lần khi test, nhưng nó là công cụ duy nhất bắt được cả một lớp lỗi mà bộ test chạy xanh bên cạnh.

Nhìn lại chặng đồng thời

Mười hai bài, và tôi thấy chúng dồn về ba điều:

Goroutine rẻ đổi cách thiết kế. 2,47 µs và 576 byte nghĩa là bạn không cần pool, không cần callback, không cần lập trình bất đồng bộ. Viết mã tuần tự, chạy nhiều bản song song.

Nhưng rẻ không có nghĩa vô hạn. Bài 38 đo được 100 goroutine kẹt vĩnh viễn từ một hàm ba dòng, và bộ thu gom rác không cứu được.

Công cụ chẩn đoán của Go đi trước. -race chỉ thẳng dòng gây lỗi đua; pprof gom nhóm goroutine theo ngăn xếp. Cả hai đều là một cờ dòng lệnh, trong khi phần tương đương ở Java cần dựng khung đo riêng.

Thử ba mươi giây

Tìm trong dự án của bạn một sync.Mutex và hỏi: khoá này bảo vệ những trường nào?

Trả lời được ngay và ghi thành comment thì thiết kế ổn. Phải đọc lại cả tệp mới biết thì đó là dấu hiệu trạng thái đang chia sẻ rộng hơn cần thiết — và cách chữa tốt nhất không phải đổi công cụ, mà là thu hẹp phạm vi chia sẻ.

Ngày mai bắt đầu chặng thư viện chuẩn: fmt, iobufio.