Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

sync.WaitGroup

جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 64 من 107.

في درس أساسيات الـ Goroutines، استخدمنا time.Sleep لانتظار انتهاء الـ goroutines—وهو نهج هش. يوفر sync.WaitGroup طريقة مناسبة لانتظار مجموعة من الـ goroutines لإكمال عملها.

تعمل 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() يضمن تناقص العداد حتى لو حدث panic في الـ goroutine.

من الأخطاء الشائعة استدعاء Add داخل الـ goroutine بدلاً من قبلها. يؤدي هذا إلى حدوث حالة سباق (race condition) حيث قد تعود Wait قبل أن يتم تسجيل جميع الـ goroutines:

// خطأ - حالة سباق (race condition)
go func() {
    wg.Add(1)  // قد يتم تشغيله بعد Wait()
    defer wg.Done()
}()

// صحيح - أضف قبل الإطلاق
wg.Add(1)
go func() {
    defer wg.Done()
}()

غالباً ما يتم تضمين WaitGroups في الـ structs لتنسيق العمليات المتزامنة، مما يجعلها ضرورية لبناء أنواع thread-safe في Go.

challenge icon

التحدي

سهل

لنقم ببناء مدير تنزيلات ينسق عمليات تنزيل ملفات متعددة ومتزامنة باستخدام sync.WaitGroup. سيقوم المدير الخاص بك بتتبع وقت اكتمال جميع التنزيلات دون الاعتماد على مؤقتات نوم عشوائية.

ستقوم بتنظيم الكود الخاص بك عبر ملفين:

  • downloader.go: حدد منطق تنسيق التنزيل الخاص بك.

    قم بإنشاء هيكل 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: قراءة معلومات التنزيل وتنسيق التنزيلات المتزامنة.

    اقرأ عدد التنزيلات، ثم لكل تنزيل اقرأ معرفه (ID)، واسم الملف، وحجمه. قم بإنشاء التنزيلات وتمريرها إلى StartDownloads. بعد اكتمال جميع التنزيلات، اطبع: All downloads complete: [total]KB total

سيتم توفير المدخلات التالية:

  • السطر 1: عدد التنزيلات (عدد صحيح)
  • الأسطر التالية: لكل تنزيل، ثلاثة أسطر - المعرف (عدد صحيح)، واسم الملف (سلسلة نصية)، والحجم بالكيلوبايت (عدد صحيح)

على سبيل المثال، إذا كان المعطى:

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()، مما يمنحك مزامنة دقيقة بغض النظر عن الوقت الذي يستغرقه كل تنزيل.

ورقة مرجعية

يوفر sync.WaitGroup طريقة مناسبة لانتظار اكتمال الـ goroutines. وهو يعمل كعداد يتتبع الـ goroutines قيد التشغيل.

الطرق الثلاث الرئيسية هي:

  • Add(n) - يزيد العداد بمقدار n
  • Done() - ينقص العداد بمقدار واحد (يعادل Add(-1))
  • Wait() - يحظر التنفيذ حتى يصل العداد إلى الصفر

نمط الاستخدام الأساسي:

var wg sync.WaitGroup

for i := 1; i <= 3; i++ {
    wg.Add(1)  // زيادة العداد قبل بدء الـ goroutine
    go func(id int) {
        defer wg.Done()  // إنقاص العداد عند الانتهاء
        fmt.Printf("Worker %d finished\n", id)
    }(i)
}

wg.Wait()  // الحظر حتى تكتمل جميع الـ goroutines
fmt.Println("All workers complete")

يضمن استخدام defer wg.Done() إنقاص العداد حتى لو حدث panic في الـ goroutine.

هام: قم دائمًا باستدعاء Add قبل تشغيل الـ goroutine، وليس بداخله، لتجنب حالات السباق (race conditions):

// Wrong - race condition
go func() {
    wg.Add(1)  // قد يتم تشغيله بعد Wait()
    defer wg.Done()
}()

// Correct
wg.Add(1)
go func() {
    defer wg.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 مع شريحة التنزيلات (downloads slice)
	// TODO: اطبع الملخص: "All downloads complete: [total]KB total"
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming