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.
التحدي
سهللنقم ببناء مدير تنزيلات ينسق عمليات تنزيل ملفات متعددة ومتزامنة باستخدام 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)- يزيد العداد بمقدار nDone()- ينقص العداد بمقدار واحد (يعادل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"
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool