Mẫu cơ bản
sync.WaitGroup đếm số goroutine đang chạy. Add tăng bộ đếm, Done giảm nó, Wait chặn cho đến khi bộ đếm về 0.
Ba lượt tải chạy đồng thời, nên chương trình mất khoảng 10 ms thay vì 30. Kết quả được in theo đúng thứ tự đầu vào, vì mỗi goroutine chỉ ghi vào chỉ số của riêng nó trong sizes, và main chỉ đọc chúng sau Wait.
Giá trị zero của WaitGroup dùng được ngay. Không cần hàm khởi tạo.
Ba quy tắc
Gọi Add trước go, không gọi bên trong goroutine. Nếu goroutine tự gọi Add, main có thể tới Wait trước khi goroutine nào kịp bắt đầu, thấy bộ đếm bằng 0 và trả về trong khi công việc còn chưa khởi động. Khi đã biết trước số lượng, gọi wg.Add(len(files)) một lần trước vòng lặp cũng tương đương.
Gọi Done bằng defer ở dòng đầu tiên của goroutine. Goroutine nào trả về sớm vì lỗi, hay bị panic, vẫn giảm được bộ đếm. Thiếu một Done là Wait bị chặn mãi mãi. Nếu đó là goroutine cuối cùng còn lại, runtime báo fatal error: all goroutines are asleep với sync.WaitGroup.Wait trong trace.
Không bao giờ sao chép WaitGroup sau lần dùng đầu tiên. Hãy truyền *sync.WaitGroup vào hàm, hoặc bắt biến trong một closure như ví dụ trên.
Truyền WaitGroup vào hàm
Khi thân goroutine là một hàm có tên, hãy truyền con trỏ:
Nếu wg sync.WaitGroup là tham số kiểu giá trị, mỗi worker sẽ gọi Done trên bản sao của riêng nó và main sẽ bị chặn trong Wait mãi mãi. go vet bắt được lỗi này trước khi bạn chạy bất cứ thứ gì:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
Một thiết kế gọn hơn là để worker không dính gì đến xử lý đồng thời: cho nó là một hàm bình thường và làm phần sổ sách Add/Done trong closure của nơi gọi. Khi đó worker dễ test và gọi đồng bộ được.
Bộ đếm âm
Done chính là Add(-1). Nếu bộ đếm xuống dưới 0, chương trình panic:
Đầu ra là recovered: sync: negative WaitGroup counter. Nguyên nhân thường gặp là một goroutine có defer wg.Done() nhưng ở nhánh nào đó lại gọi thêm wg.Done() trực tiếp.
Gom lỗi
WaitGroup chỉ biết đếm. Với lỗi, hãy cho mỗi goroutine một ô riêng và kiểm tra chúng sau Wait:
errors.Join (Go 1.20) bỏ qua các giá trị nil và trả về nil nếu tất cả đều là nil, nên nó gộp "mỗi goroutine một lỗi" mà không cần sổ sách gì thêm.
Nếu bạn muốn dừng phần việc còn lại ngay khi một goroutine thất bại, hãy dùng golang.org/x/sync/errgroup. Nó là một WaitGroup cộng với lỗi đầu tiên cộng với một context bị hủy khi có thất bại, và g.SetLimit(n) giới hạn mức đồng thời. Package này nằm ngoài thư viện chuẩn, nên không chạy được trong trình soạn thảo của trang này:
g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
return err // the first error; ctx was cancelled for the others
}
Worker pool
Một số goroutine cố định đọc việc từ một channel giúp giữ mức đồng thời trong giới hạn, dù có bao nhiêu việc đi nữa. WaitGroup cho bạn biết khi nào mọi worker đã xong, và đó là lúc có thể đóng channel kết quả.
Thứ tự của ba phần này rất quan trọng:
mainphải nhận kết quả trong khi các worker đang chạy. Nếumaingọi thẳngwg.Wait()trước khi đọc, các worker sẽ bị chặn khi gửi vàoresults, không bao giờ tới đượcDone, và mọi thứ rơi vào deadlock. Vì thếWaitchạy trong một goroutine riêng.close(results)chỉ xảy ra sauWait, nên không worker nào gửi vào một channel đã đóng.- Phần cấp việc cũng chạy trong một goroutine, nên việc cấp việc và gom kết quả diễn ra song song.
Worker nào xử lý việc nào thay đổi theo từng lần chạy, nên chương trình sắp xếp theo việc trước khi in. Mọi thứ nó in ra đều cố định.
WaitGroup, channel hay errgroup
| Nhu cầu | Dùng |
|---|---|
| Chờ N goroutine, kết quả nằm trong các ô theo chỉ số | sync.WaitGroup |
| Chờ một goroutine | một channel done hoặc chính channel kết quả |
| Nhận kết quả ngay khi từng goroutine xong | một channel, đóng sau wg.Wait() |
| Dừng mọi thứ khi có lỗi đầu tiên | errgroup.WithContext |
| Dừng mọi thứ khi hết thời gian hoặc nơi gọi hủy | context.Context cùng một WaitGroup hoặc errgroup |
Go 1.25 bổ sung wg.Go(func() { ... }), tự làm Add(1) và Done có defer giúp bạn. Code cho Go 1.24 trở về trước, kể cả trình soạn thảo của trang này, dùng dạng tường minh như ở trên.
Lỗi thường gặp
- Gọi
wg.Add(1)bên trong goroutine.Waitcó thể trả về trước khi nó kịp chạy. - Quên
Donekhi trả về sớm. Luôn viếtdefer wg.Done(). - Truyền WaitGroup theo giá trị. Hãy dùng con trỏ;
go vetsẽ đánh dấu bản sao. - Chờ trong chính goroutine phải đọc hết một channel. Chuyển
wg.Wait()cùngclosesang một goroutine riêng. - Dùng lại WaitGroup khi
Waittrước đó chưa trả về. Chỉ bắt đầu một vòng gọiAddmới sau khiWaitđã xong.
Câu hỏi thường gặp
sync.WaitGroup trong Go hoạt động như thế nào?
WaitGroup là một bộ đếm. wg.Add(n) tăng nó lên, wg.Done() giảm đi một, và wg.Wait() chặn cho đến khi bộ đếm về 0. Gọi Add trước khi khởi chạy mỗi goroutine, defer wg.Done() bên trong goroutine, và Wait ở chỗ bạn cần mọi thứ đã xong.
Nên truyền WaitGroup theo giá trị hay theo con trỏ?
Theo con trỏ (*sync.WaitGroup), hoặc để goroutine bắt nó trong một closure. Bản sao có bộ đếm riêng, nên Done trên bản sao không bao giờ chạm tới bản gốc và Wait bị chặn mãi mãi. go vet báo lỗi này là "passes lock by value".
Lỗi "sync: negative WaitGroup counter" do đâu?
Do số lần gọi Done nhiều hơn số lần gọi Add. Thường là một goroutine gọi Done hai lần (một lần bằng defer và một lần gọi trực tiếp), hoặc có một nhánh bỏ sót Add(1). Chương trình panic, vì bộ đếm không còn phản ánh đúng điều gì nữa.
Lấy lỗi từ các goroutine chạy bằng WaitGroup như thế nào?
WaitGroup không mang theo kết quả hay lỗi. Hãy cho mỗi goroutine một ô riêng trong một slice lỗi rồi gộp chúng sau Wait (ví dụ bằng errors.Join), hoặc dùng golang.org/x/sync/errgroup, có Wait trả về lỗi đầu tiên và có thể hủy các goroutine còn lại qua một context.