sync.WaitGroup
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 64 מתוך 107.
בשיעור יסודות הגורוטינות השתמשנו ב־time.Sleep כדי להמתין לסיום הגורוטינות: גישה לא אמינה. sync.WaitGroup מספק דרך מתאימה להמתין לקבוצה של גורוטינות עד שישלימו את עבודתן.
WaitGroup פועל כמו מונה. מגדילים אותו לפני שמתחילים goroutine, מקטינים אותו כשה־goroutine מסתיים, וממתינים עד שהמונה מגיע לאפס:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // הגדל את המונה
go func(id int) {
defer wg.Done() // הקטן את המונה בסיום
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // המתן עד שהמונה יהיה 0
fmt.Println("All workers complete")
}שלוש השיטות העיקריות הן Add(n) להגדלת המונה ב־n, Done() להקטנתו באחד (שקול ל־Add(-1)), ו־Wait() לחסימה עד שהמונה מגיע לאפס. שימוש ב־defer wg.Done() מבטיח שהמונה יקטן גם אם הגורוטינה נכנסת לפאניקה.
טעות נפוצה היא לקרוא ל־Add בתוך הגורוטינה במקום לפני כן. כך נוצרת תחרות שבה Wait עשוי לחזור לפני שכל הגורוטינות נרשמו:
// שגוי - מצב מרוץ
go func() {
wg.Add(1) // עשוי לרוץ אחרי Wait()
defer wg.Done()
}()
// נכון - יש לקרוא ל-Add לפני ההפעלה
wg.Add(1)
go func() {
defer wg.Done()
}()לעיתים קרובות משלבים WaitGroups בתוך מבנים כדי לתאם פעולות מקבילות, ולכן הם חיוניים ליצירת טיפוסים בטוחים לשימוש במקביל ב-Go.
אתגר
קלבואו נבנה מנהל הורדות שמתאם הורדות קבצים מרובות בו-זמנית באמצעות sync.WaitGroup. המנהל יעקוב אחר השלמת כל ההורדות בלי להסתמך על טיימרים שרירותיים של השהיה.
תארגנו את הקוד שלכם בשני קבצים:
downloader.go: הגדירו את לוגיקת תיאום ההורדות.צרו struct בשם
Downloadעם השדותID(int),Filename(string) ו-Size(int, המייצג KB).ממשו מתודה
Process(wg *sync.WaitGroup)עבורDownload, המדמה הורדה באמצעות השהיה למשך זמן שנקבע לפי גודל הקובץ (השתמשו ב-Size * 10מילישניות). כשההורדה מסתיימת, הדפיסו:Downloaded: [Filename] ([Size]KB). השתמשו ב-defer wg.Done()כדי להבטיח שמונה ה-WaitGroup יופחת כראוי.צרו פונקציה
StartDownloads(downloads []Download) intשמפעילה את כל ההורדות בו-זמנית באמצעות goroutines וממתינה עד שכולן יסתיימו. זכרו לקרוא ל-wg.Add(1)לפני הפעלת כל goroutine, ולא בתוכו. החזירו את הגודל הכולל של כל ההורדות.main.go: קראו את פרטי ההורדות ותזמנו את ההורדות בו-זמנית.קראו את מספר ההורדות, ואז עבור כל הורדה קראו את המזהה, שם הקובץ והגודל שלה. צרו את ההורדות והעבירו אותן אל
StartDownloads. לאחר שכל ההורדות יסתיימו, הדפיסו:All downloads complete: [total]KB total
הקלטים הבאים יסופקו:
- שורה 1: מספר ההורדות (מספר שלם)
- השורות הבאות: עבור כל הורדה, שלוש שורות — המזהה (מספר שלם), שם הקובץ (מחרוזת) והגודל ב-KB (מספר שלם)
לדוגמה, עבור הקלט:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10הפלט שלכם אמור להציג את סיום ההורדות (קבצים קטנים יותר מסתיימים ראשונים בגלל זמני ההשהיה הקצרים יותר), ולאחר מכן את הסיכום:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalההבדל המרכזי לעומת שימוש ב-time.Sleep הוא ש-wg.Wait() חוסם עד שכל ה-goroutines מאותתים על השלמתם באמצעות Done(), וכך מתקבל סנכרון מדויק בלי קשר למשך הזמן שכל הורדה אורכת.
נסו בעצמכם
package main
import (
"fmt"
)
func main() {
// קראו את מספר ההורדות
var n int
fmt.Scanln(&n)
// קראו את פרטי ההורדות
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO: קראו ל-StartDownloads עם ה-slice של ההורדות
// TODO: הדפיסו את הסיכום: "All downloads complete: [total]KB total"
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP ב-Go
קבצים חיצונייםסביבת העבודה והמודולים של Goחבילות וייבואשמות מיוצאים לעומת שמות לא מיוצאיםמבוא ל-OOP ב-GoStructs כמחלקותהגדרת מתודות עבור Structsמקבלי Pointer לעומת Valueאתחול Structפונקציות Constructorחזרה – מחשבון פשוט4ממשקים
מבוא לממשקיםמימוש משתמעממשק כחוזהממשק ריק (any)אימות טיפוסמתג טיפוסיםהרכבת ממשקיםממשקי Stringer ו-Errorסיכום – מחשבון צורות7כימוס
שדות מיוצאים לעומת שדות לא מיוצאיםכימוס ברמת החבילהמתודות Getter ו-Setterהסתרת מידע ב-Goסיכום – רשומות תלמידים10Generics (Go 1.18 ומעלה)
מבוא ל־Genericsפרמטרים של טיפוסיםאילוצי טיפוסיםמבנים גנרייםפתרון עוקף למתודות גנריותסיכום – אוסף גנרי2העמקה בסוגים וב-Structs
סוגים בסיסיים ומורכביםהגדרות של סוגים מותאמים אישיתתגיות StructStructs אנונימייםStructs מקונניםערכי אפס וברירות מחדלסיכום – ספר אנשי קשר5קומפוזיציה במקום ירושה
למה ב-Go אין ירושהיסודות הטמעת Structקידום מתודותהטמעת כמה Structsהטמעה לעומת אגרגציההסתרת מתודות מוטמעותסיכום – היררכיית עובדים8טיפול בשגיאות ו-OOP
ממשק errorסוגי שגיאות מותאמים אישיתעטיפת שגיאות (fmt.Errorf)שגיאות Sentinelerrors.Is() ו-errors.As()Panic, Defer ו-Recoverסיכום – מנתח קבצים11ספרייה סטנדרטית ותכנות מונחה עצמים
io.Reader ו-io.Writersort.Interfaceהממשק fmt.Stringerencoding/json עם מבניםהממשק http.Handlerסיכום – מודלים של REST API3מצביעים וזיכרון
יסודות המצביעים ב-Goמצביעים למבניםהעברה לפי ערך לעומת לפי הפניההפונקציה new()איסוף אשפה ב-Goסיכום - בונה רשימה מקושרת6פולימורפיזם ב-Go
פולימורפיזם באמצעות ממשקיםטיפוס ברווז ב-Goכללי התאמה לממשקאוספים פולימורפייםהזרקת תלויותסיכום - מעבד תשלומים9מקביליות ו-OOP
יסודות GoroutinesChannels ותקשורתChannel עם חוצץ לעומת ללא חוצץפקודת Selectsync.Mutex ו-sync.RWMutexsync.WaitGroupתכנון Struct בטוח לשימוש במקבילסיכום - מאגר עובדים12מושגים מתקדמים בתכנות מונחה עצמים
דפוס האפשרויות הפונקציונליותדפוס Builder ב-Goשרשור מתודותכינויי טיפוסים לעומת הגדרותיסודות Reflectionסקירה כללית של יצירת קודתרגלו בעצמכם: קומפיילר Go אונליין