sync.WaitGroup
جزء من قسم البرمجة كائنية التوجه في رحلة GO على Coddy. الدرس 64 من 107.
في درس أساسيات Goroutines، استخدمنا time.Sleep للانتظار حتى تنتهي goroutines: وهو أسلوب هش. توفّر sync.WaitGroup طريقة مناسبة للانتظار حتى complete مجموعة من goroutines عملها.
يعمل WaitGroup مثل counter. تقوم بعملية increment له before بدء goroutine، وتقوم بعملية decrement له عند انتهاء goroutine، وتظل في حالة block حتى يصل counter إلى الصفر:
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) لزيادة counter بمقدار n، وDone() لتقليله بمقدار واحد (وهو ما يعادل Add(-1))، وWait() لتنفيذ block حتى يصل counter إلى الصفر. يضمن استخدام defer wg.Done() تناقص counter حتى إذا حدث panic في goroutine.
من الأخطاء الشائعة استدعاء Add داخل goroutine بدلًا من استدعائه قبلها. يؤدي ذلك إلى إنشاء حالة تسابق قد تُرجع فيها Wait النتيجة قبل تسجيل جميع goroutines:
// خطأ - حالة سباق
go func() {
wg.Add(1) // قد يعمل بعد Wait()
defer wg.Done()
}()
// صحيح - Add قبل الإطلاق
wg.Add(1)
go func() {
defer wg.Done()
}()غالبًا ما يتم تضمين WaitGroups في البُنى لتنسيق العمليات concurrent، مما يجعلها أساسية لبناء أنواع آمنة للخيوط في Go.
التحدي
سهللنبنِ مدير تنزيلات ينسّق عمليات تنزيل file متعددة ومتزامنة باستخدام sync.WaitGroup. سيتتبّع مديرك متى تكتمل جميع عمليات التنزيل دون الاعتماد على مؤقتات نوم عشوائية.
ستنظّم شفرتك عبر ملفين:
downloader.go: عرّف منطق تنسيق التنزيلات لديك.أنشئ بنية
Downloadمع حقولID(int) وFilename(string) وSize(int، تمثّل KB).Implement أسلوبًا
Process(wg *sync.WaitGroup)علىDownloadيحاكي تنزيلfileمن خلال النوم لمدة تعتمد على حجمfile(استخدمSize * 10ميلي ثانية). عند اكتمال التنزيل، اطبع:Downloaded: [Filename] ([Size]KB). استخدمdefer wg.Done()لضمان إنقاص عدّاد WaitGroup بشكل صحيح.أنشئ
functionStartDownloads(downloads []Download) intتطلق جميع التنزيلات concurrently باستخدام goroutines وتنتظر حتى تكتمل كلها. تذكّر استدعاءwg.Add(1)قبل إطلاق كل goroutine، وليس بداخلها. أعد الحجم الإجمالي لجميع التنزيلات مجتمعة.main.go: اقرأ information التنزيلات ونظّم التنزيلات concurrent.اقرأ عدد التنزيلات، ثم اقرأ لكل تنزيل معرّفه واسم file وحجمه. أنشئ التنزيلات ومرّرها إلى
StartDownloads. بعد اكتمال جميع التنزيلات، اطبع:All downloads complete: [total]KB total
سيتم توفير المدخلات التالية:
- السطر 1: عدد التنزيلات (عدد صحيح)
- الأسطر التالية: لكل تنزيل، ثلاثة أسطر - المعرّف (عدد صحيح)، واسم file (سلسلة نصية)، والحجم بوحدة 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 مع شريحة downloads
// TODO: Print the summary: "All downloads complete: [total]KB total"
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس البرمجة كائنية التوجه
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تدرّب بنفسك: مترجم Go عبر الإنترنت