Có những bài toán đồng thời cần các goroutine đồng bước: tất cả phải hoàn thành pha N trước khi bất kỳ ai bắt đầu pha N+1. Ví dụ điển hình: thuật toán lặp song song, mô phỏng vật lý theo bước thời gian — mỗi bước phụ thuộc vào việc mọi worker đã cập nhật xong bước trước. Công cụ cho việc này là barrier (rào chắn): một điểm mà N goroutine cùng chờ tới rồi cùng đi tiếp. Bài này dựng barrier bằng sync.Cond và đo thật tác dụng của nó.

Ba cách dựng barrier trong Go

Go không có kiểu Barrier sẵn, nhưng dựng dễ từ nguyên thủy có sẵn:

  • sync.WaitGroup: rào một lần — main chờ tất cả goroutine tới một điểm. Đơn giản nhất, nhưng không đối xứng (worker không chờ nhau, chỉ main chờ).
  • channel close: rào một lần dạng broadcast — mọi goroutine chờ <-startCh, rồi close(startCh) mở cho tất cả cùng lúc.
  • sync.Cond: rào tuần hoàn (dùng lại nhiều vòng) — mạnh nhất, cho lockstep nhiều pha.

Ảnh chụp đoạn mã Go nền tối minh hoạ barrier đồng bộ nhiều goroutine tại một rào chắn, rào chắn N goroutine phải cùng tới điểm rào rồi mới cùng đi tiếp giữ các worker song song đồng bước, một ba cách dựng barrier trong Go sync.WaitGroup rào một lần main chờ tất cả tới điểm đơn giản nhất channel close rào một lần broadcast close mở cho tất cả cùng lúc sync.Cond rào tuần hoàn dùng lại nhiều vòng mạnh nhất, hai barrier tuần hoàn bằng sync.Cond func b Barrier Wait b.mu.Lock defer b.mu.Unlock g bằng b.gen b.count cộng cộng if b.count bằng b.n người cuối tới mở rào b.count bằng 0 b.gen cộng cộng b.cond.Broadcast đánh thức mọi goroutine chờ else for g bằng b.gen chờ tới khi thế hệ đổi rào mở b.cond.Wait nhả khóa cộng ngủ được đánh thức thì lấy lại gen thế hệ làm barrier tuần hoàn mỗi lần đủ N thì tăng gen các goroutine chờ theo gen cũ được thả dùng lại cho vòng sau, ba channel-barrier một lần đơn giản hơn start bằng make chan struct mỗi worker chờ tín hiệu bắt đầu thiếu start tất cả block ở đây điều phối mở rào cho tất cả cùng lúc close start mọi thiếu start trả về đồng thời, bốn khi nào cần barrier thuật toán lặp song song mỗi pha phụ thuộc mọi worker xong pha trước mô phỏng vật lý ma trận cập nhật đồng bộ theo bước thời gian khởi động đồng loạt benchmark mọi goroutine chạy cùng lúc

Hình 1: Barrier. Ba cách dựng (WaitGroup/channel/Cond), barrier tuần hoàn bằng sync.Cond với gen counter, channel-barrier một lần, và khi nào cần.

Barrier tuần hoàn bằng sync.Cond

func (b *Barrier) Wait() {
	b.mu.Lock(); defer b.mu.Unlock()
	g := b.gen
	b.count++
	if b.count == b.n {        // người CUỐI tới → mở rào
		b.count = 0; b.gen++
		b.cond.Broadcast()      // đánh thức MỌI goroutine chờ
	} else {
		for g == b.gen {         // chờ tới khi thế hệ đổi (rào mở)
			b.cond.Wait()        // nhả khóa + ngủ, được đánh thức thì lấy lại khóa
		}
	}
}

Cơ chế: mỗi goroutine tới tăng count. Người cuối cùng (count == n) đặt lại count, tăng gen (thế hệ), và Broadcast đánh thức tất cả. Những goroutine tới trước đang Wait trong vòng lặp kiểm g == b.gen — khi gen đổi, chúng thoát. gen làm barrier tuần hoàn: mỗi vòng có một thế hệ mới, nên barrier dùng lại được cho vòng sau. (Lưu ý cond.Wait phải trong vòng for kiểm điều kiện, không phải if — chống spurious wakeup.)

Đo thật: đồng bước vs mất đồng bộ

Cho 4 goroutine tốc độ khác nhau (id × 5ms) chạy 3 vòng:

Ảnh chụp bảng kết quả đo thật nền tối có barrier giữ đồng bước không barrier lệch 2 vòng go run sync.Cond Go 1.23 arm64 10 core 4 goroutine 3 vòng tốc độ khác nhau, có barrier tuần hoàn mỗi vòng đủ 4 mới qua barrier 4 goroutine 3 vòng vòng 0 đủ 4 goroutine tới rào cùng qua vòng 1 đủ 4 goroutine tới rào cùng qua vòng 2 đủ 4 goroutine tới rào cùng qua mọi vòng đủ 4 goroutine trước khi bất kỳ ai sang vòng sau dù 4 goroutine chạy tốc độ khác nhau id x 5ms barrier ép chúng đồng bước không ai sang vòng r cộng 1 tới khi cả 4 xong vòng r, không barrier goroutine nhanh vượt trước khoảng cách vòng lớn nhất giữa các goroutine bằng 2 goroutine nhanh đã sang vòng sau trong khi goroutine chậm còn vòng trước không barrier goroutine id bằng 0 nhanh đã ở vòng 2 khi id bằng 3 chậm còn vòng 0 lệch 2 vòng nếu vòng sau phụ thuộc dữ liệu vòng trước của mọi worker đây là bug đọc dữ liệu chưa cập nhật xong, ba cách so sánh sync.WaitGroup main chờ tất cả một lần tạo mới mỗi vòng channel close broadcast mở rào một lần channel mới mỗi lần sync.Cond đối xứng tuần hoàn có gen counter, cốt lõi barrier N goroutine cùng tới rào rồi cùng đi tiếp đồng bước đo thật có barrier mọi vòng đủ 4 không barrier lệch 2 vòng Cond barrier tuần hoàn Broadcast cộng Wait cộng gen counter channel barrier một lần đơn giản close mở cho tất cả dùng khi thuật toán lặp song song mô phỏng khởi động đồng loạt

Hình 2: Có barrier — mỗi vòng đủ 4 goroutine mới qua (đồng bước). Không barrier — goroutine nhanh vượt trước, lệch 2 vòng. Bảng so ba cách dựng.

  • Có barrier tuần hoàn: mỗi vòng đều đủ 4 goroutine tới rào rồi mới cùng qua — dù tốc độ khác nhau, barrier ép chúng đồng bước. Không ai sang vòng r+1 tới khi cả 4 xong vòng r.
  • Không barrier: khoảng cách vòng lớn nhất giữa các goroutine = 2 — goroutine id=0 (nhanh) đã ở vòng 2 khi id=3 (chậm) còn vòng 0. Mất đồng bộ hoàn toàn.

Nếu vòng sau phụ thuộc dữ liệu vòng trước của MỌI worker (như mô phỏng), việc mất đồng bộ này là bug: goroutine nhanh đọc dữ liệu vòng trước mà worker chậm chưa cập nhật xong.

Ứng dụng thực tế

Thuật toán lặp song song đồng bộ theo pha. Nhân ma trận theo khối, mô phỏng vật lý (mỗi hạt cập nhật theo trạng thái toàn cục bước trước), thuật toán đồ thị lặp — mọi worker phải xong pha hiện tại trước khi ai bắt đầu pha sau. Barrier giữ đúng ngữ nghĩa này.

Khởi động đồng loạt cho benchmark. Khi đo N goroutine song song, dùng channel-barrier (close(start)) để chúng bắt đầu cùng một khoảnh khắc — nếu goroutine đầu chạy trước khi goroutine cuối được tạo, phép đo lệch. Đây là mẫu chuẩn trong benchmark đồng thời.

Chọn barrier theo nhu cầu dùng lại. Cần một pha đồng bộ: WaitGroup hoặc channel đủ. Cần nhiều pha lặp lại: dùng barrier tuần hoàn (sync.Cond) để không phải tạo mới mỗi vòng. Đừng dùng Cond khi WaitGroup đủ — nó phức tạp hơn.

Đánh đổi cần cân nhắc

Barrier ép tốc độ về goroutine chậm nhất. Bản chất của đồng bước: mọi worker chờ người chậm nhất ở mỗi rào. Nếu tải không cân (một worker luôn chậm), barrier lãng phí thời gian các worker nhanh. Cân nhắc chia tải đều hơn, hoặc dùng mô hình khác (work-stealing) nếu đồng bước không thật sự cần — barrier chỉ đáng khi đúng đắn đòi hỏi nó.

sync.Cond khó dùng đúng, dễ sai. Cond là nguyên thủy cấp thấp: phải nhớ Wait trong vòng for (không phải if), giữ khóa đúng cách, và Broadcast vs Signal. Nhiều bug đồng thời đến từ dùng Cond sai. Nếu channel hoặc WaitGroup diễn đạt được, ưu tiên chúng — Cond chỉ khi thật cần barrier tuần hoàn hoặc điều kiện chờ phức tạp.

Barrier không chịu lỗi tốt. Nếu một goroutine chết (panic) trước khi tới rào, các goroutine khác chờ mãi mãi — deadlock (như bài trước). Barrier production cần cơ chế: context timeout, hoặc đếm goroutine còn sống và điều chỉnh n. Đừng dùng barrier "trần" nơi worker có thể thất bại.

Ba ý mang về

  1. Barrier bắt N goroutine cùng tới rào rồi cùng đi tiếp (đồng bước): đo thật, 4 goroutine tốc độ khác nhau vẫn đồng bộ từng vòng với barrier tuần hoàn — không ai sang vòng r+1 tới khi cả 4 xong vòng r; không barrier, goroutine nhanh vượt trước lệch tới 2 vòng.
  2. Ba cách dựng theo nhu cầu: sync.WaitGroup (rào một lần, main chờ), channel close (broadcast một lần, mở cho tất cả cùng lúc), sync.Cond (barrier tuần hoàn dùng lại qua gen counter với Broadcast + Wait) — chọn Cond chỉ khi cần nhiều pha lặp lại.
  3. Barrier ép tốc độ về goroutine chậm nhất và không chịu lỗi tốt: nó đáng khi đúng đắn đòi hỏi đồng bước (thuật toán lặp song song, mô phỏng, khởi động benchmark đồng loạt) — nhưng sync.Cond khó dùng đúng (Wait trong for), và một goroutine chết trước rào gây deadlock, nên cần timeout/context ở production.

Phần sau ta chuyển sang đo lường chính vấn đề đồng thời đã gặp nhiều lần: Phần sau mổ xẻ mutex profile — cách dùng runtime.SetMutexProfileFraction và pprof để đo tranh chấp khóa thật trong chương trình, tìm mutex nào là nút thắt.