Menu
Coddy logo textTech

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.

challenge icon

אתגר

קל

בואו נבנה מנהל הורדות שמתאם הורדות קבצים מרובות בו-זמנית באמצעות 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"
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: קומפיילר Go אונליין