Menu

WaitGroup trong Golang: Add, Done, Wait và worker pool

Cách sync.WaitGroup chờ một nhóm goroutine chạy xong: quy tắc của Add, Done và Wait, vì sao phải truyền nó bằng con trỏ, cách gom kết quả và lỗi, và một worker pool xây trên nó.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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:

  • main phải nhận kết quả trong khi các worker đang chạy. Nếu main gọi thẳng wg.Wait() trước khi đọc, các worker sẽ bị chặn khi gửi vào results, không bao giờ tới được Done, và mọi thứ rơi vào deadlock. Vì thế Wait chạy trong một goroutine riêng.
  • close(results) chỉ xảy ra sau Wait, 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ầuDùng
Chờ N goroutine, kết quả nằm trong các ô theo chỉ sốsync.WaitGroup
Chờ một goroutinemột channel done hoặc chính channel kết quả
Nhận kết quả ngay khi từng goroutine xongmột channel, đóng sau wg.Wait()
Dừng mọi thứ khi có lỗi đầu tiênerrgroup.WithContext
Dừng mọi thứ khi hết thời gian hoặc nơi gọi hủycontext.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. Wait có thể trả về trước khi nó kịp chạy.
  • Quên Done khi trả về sớm. Luôn viết defer wg.Done().
  • Truyền WaitGroup theo giá trị. Hãy dùng con trỏ; go vet sẽ đánh dấu bản sao.
  • Chờ trong chính goroutine phải đọc hết một channel. Chuyển wg.Wait() cùng close sang một goroutine riêng.
  • Dùng lại WaitGroup khi Wait trước đó chưa trả về. Chỉ bắt đầu một vòng gọi Add mới sau khi Wait đã 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.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu