הדפוס הבסיסי
sync.WaitGroup סופר goroutines שרצות. Add מעלה את הספירה, Done מורידה אותה, ו-Wait נחסם עד שהיא אפס.
שלוש ההורדות רצות במקביל, כך שהתוכנית לוקחת בערך 10 ms במקום 30. התוצאות מודפסות לפי סדר הקלט כי כל goroutine כותבת רק לאינדקס שלה ב-sizes, ו-main קוראת אותן רק אחרי Wait.
ערך האפס של WaitGroup מוכן לשימוש. אין בנאי.
שלושת הכללים
קראו ל-Add לפני go, לא בתוך ה-goroutine. אם ה-goroutine קוראת ל-Add בעצמה, main יכולה להגיע ל-Wait לפני שאף goroutine התחילה, לראות ספירה של אפס ולחזור בזמן שהעבודה עוד לא התחילה. כשהספירה ידועה מראש, wg.Add(len(files)) פעם אחת לפני הלולאה שקול לזה.
קראו ל-Done עם defer כשורה הראשונה של ה-goroutine. goroutine שחוזרת מוקדם בגלל שגיאה, או גורמת ל-panic, עדיין מקטינה את המונה. Done חסר משאיר את Wait חסום לנצח. אם זו ה-goroutine היחידה שנשארה, סביבת הריצה מדווחת fatal error: all goroutines are asleep עם sync.WaitGroup.Wait ב-trace.
לעולם אל תעתיקו WaitGroup אחרי השימוש הראשון. העבירו *sync.WaitGroup לפונקציות, או לכדו את המשתנה ב-closure כמו למעלה.
העברת WaitGroup לפונקציה
כשגוף ה-goroutine הוא פונקציה עם שם, העבירו מצביע:
עם wg sync.WaitGroup כפרמטר לפי ערך, כל worker היה קורא ל-Done על העותק שלו ו-main הייתה נחסמת ב-Wait לנצח. go vet תופס את זה לפני שמריצים משהו:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
תכנון נקי יותר משאיר את המקביליות מחוץ ל-worker לגמרי: תנו לה להיות פונקציה רגילה ובצעו את הניהול של Add/Done ב-closure של הקורא. אז קל לבדוק את worker ולקרוא לה באופן סינכרוני.
מונה שלילי
Done היא Add(-1). אם הספירה יורדת מתחת לאפס, התוכנית גורמת ל-panic:
הפלט הוא recovered: sync: negative WaitGroup counter. הסיבה הרגילה היא goroutine עם defer wg.Done() שקוראת גם ל-wg.Done() במפורש באחד המסלולים.
איסוף שגיאות
WaitGroup רק סופר. לשגיאות, תנו לכל goroutine מקום משלה ובדקו אותן אחרי Wait:
errors.Join (Go 1.20) מדלגת על ערכי nil ומחזירה nil אם כולם nil, כך שהיא משלבת "שגיאה אחת לכל goroutine" בלי שום ניהול נוסף.
אם רוצים לעצור את שאר העבודה ברגע ש-goroutine אחת נכשלת, השתמשו במקום זה ב-golang.org/x/sync/errgroup. הוא WaitGroup ועוד השגיאה הראשונה ועוד context שמבוטל בכישלון, ו-g.SetLimit(n) מגביל את המקביליות. הוא נמצא מחוץ לספרייה הסטנדרטית, ולכן לא יכול לרוץ בעורך של הדף הזה:
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
מספר קבוע של goroutines שקוראות משימות מ-channel שומר על מקביליות מוגבלת, לא משנה כמה משימות יש. ה-WaitGroup אומר מתי כל ה-workers סיימו, וזה הרגע שבו אפשר לסגור את ה-channel של התוצאות.
הסדר של שלושת החלקים חשוב:
mainחייבת לקבל תוצאות בזמן שה-workers רצים. אםmainהייתה קוראת ל-wg.Wait()ישירות לפני הקריאה, ה-workers היו נחסמים בשליחה ל-results, אף פעם לא היו מגיעים ל-Done, והכול היה נתקע ב-deadlock. לכןWaitרץ ב-goroutine משלו.close(results)קורה רק אחריWait, כך שאף worker לא יכול לשלוח ל-channel סגור.- מזין המשימות רץ גם הוא ב-goroutine, כך שההזנה והאיסוף חופפים.
איזה worker טיפל באיזו משימה משתנה מהרצה להרצה, ולכן התוכנית ממיינת לפי משימה לפני ההדפסה. כל מה שהיא מדפיסה דטרמיניסטי.
WaitGroup, channel או errgroup
| צורך | במה להשתמש |
|---|---|
| להמתין ל-N goroutines, תוצאות במקומות לפי אינדקס | sync.WaitGroup |
| להמתין ל-goroutine אחת | channel בשם done או ה-channel של התוצאה עצמו |
| תוצאות שזורמות ככל שהן מסתיימות | channel, שנסגר אחרי wg.Wait() |
| לעצור הכול בשגיאה הראשונה | errgroup.WithContext |
| לעצור הכול ב-timeout או בביטול של הקורא | context.Context ועוד WaitGroup או errgroup |
Go 1.25 מוסיפה את wg.Go(func() { ... }), שעושה בשבילכם את ה-Add(1) ואת ה-Done שב-defer. קוד ל-Go 1.24 ולפני כן, כולל העורך של הדף הזה, משתמש בצורה המפורשת שמוצגת למעלה.
טעויות נפוצות
wg.Add(1)בתוך ה-goroutine.Waitיכול לחזור לפני שהיא רצה.- שכחה של
Doneבחזרה מוקדמת. תמידdefer wg.Done(). - העברת ה-WaitGroup לפי ערך. השתמשו במצביע;
go vetמסמן את ההעתקה. - המתנה באותה goroutine שצריכה לרוקן channel. העבירו את
wg.Wait()ואת ה-closeל-goroutine נפרדת. - שימוש חוזר ב-WaitGroup לפני שה-
Waitהקודם חזר. התחילו מחזור חדש של קריאותAddרק אחרי ש-Waitהסתיים.
שאלות נפוצות
איך sync.WaitGroup עובד ב-Go?
WaitGroup הוא מונה. wg.Add(n) מגדיל אותו, wg.Done() מקטין אותו באחד, ו-wg.Wait() נחסם עד שהוא מגיע לאפס. קראו ל-Add לפני שמפעילים כל goroutine, ל-defer wg.Done() בתוכה, ול-Wait במקום שבו צריך שהכול יסתיים.
האם להעביר WaitGroup לפי ערך או לפי מצביע?
לפי מצביע (*sync.WaitGroup), או לתת ל-goroutines ללכוד אותו ב-closure. לעותק יש מונה משלו, כך ש-Done על העותק אף פעם לא מגיע למקור ו-Wait נחסם לנצח. go vet מדווח על הטעות כ-"passes lock by value".
מה גורם ל-"sync: negative WaitGroup counter"?
יותר קריאות ל-Done מקריאות ל-Add. בדרך כלל goroutine קוראת ל-Done פעמיים (פעם עם defer ופעם במפורש), או ש-Add(1) מדולג באחד המסלולים. התוכנית גורמת ל-panic, כי המונה כבר לא יכול לומר שום דבר נכון.
איך מקבלים שגיאות מ-goroutines שהופעלו עם WaitGroup?
WaitGroup לא נושא תוצאות או שגיאות. תנו לכל goroutine מקום משלה ב-slice של שגיאות ושלבו אותן אחרי Wait (למשל עם errors.Join), או השתמשו ב-golang.org/x/sync/errgroup, שה-Wait שלו מחזיר את השגיאה הראשונה ויכול לבטל את האחרות דרך context.