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

WaitGroup في Golang: Add وDone وWait ومجموعة عمّال

كيف تنتظر sync.WaitGroup مجموعة من الـ goroutines حتى تنتهي: قواعد Add وDone وWait، ولماذا يجب تمريرها بمؤشر، وجمع النتائج والأخطاء، ومجموعة عمّال مبنية عليها.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

النمط الأساسي

تعدّ sync.WaitGroup الـ goroutines العاملة. ترفع Add العدد، وتخفضه Done، وتتوقّف Wait حتى يصير صفرًا.

تعمل التنزيلات الثلاثة بالتزامن، فيستغرق البرنامج نحو 10 ms بدل 30. تُطبع النتائج بترتيب المدخلات لأن كل goroutine تكتب فقط في فهرسها من sizes، ولا تقرؤها main إلا بعد Wait.

القيمة الصفرية لـ WaitGroup جاهزة للاستخدام. لا حاجة لدالة بناء.

القواعد الثلاث

استدعِ Add قبل go، لا داخل الـ goroutine. إذا استدعت الـ goroutine الدالة Add بنفسها، قد تصل main إلى Wait قبل أن تبدأ أي goroutine، فترى عددًا صفريًا وتعود بينما لم يبدأ العمل. عندما تعرف العدد مسبقًا، فإن wg.Add(len(files)) مرة واحدة قبل الحلقة مكافئة.

استدعِ Done مع defer في أول سطر من الـ goroutine. الـ goroutine التي تعود مبكرًا بسبب خطأ، أو تسبّب panic، تُنقص العدّاد مع ذلك. غياب Done يترك Wait متوقّفة إلى الأبد. وإذا كانت آخر goroutine متبقّية، يبلغ وقت التشغيل عن fatal error: all goroutines are asleep مع sync.WaitGroup.Wait في الأثر.

لا تنسخ WaitGroup بعد أول استخدام أبدًا. مرّر *sync.WaitGroup إلى الدوال، أو التقط المتغيّر في إغلاق كما سبق.

تمرير WaitGroup إلى دالة

عندما يكون جسم الـ goroutine دالة مسمّاة، مرّر مؤشرًا:

مع wg sync.WaitGroup كمعامل قيمة، كان كل عامل سيستدعي Done على نسخته الخاصة وكانت main ستتوقّف في Wait إلى الأبد. يكتشف go vet ذلك قبل أن تشغّل أي شيء:

./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy

تصميم أنظف يُبقي التزامن خارج worker كليًا: دعها دالة عادية وأجرِ حسابات Add/Done في إغلاق المستدعي. عندها يسهل اختبار worker واستدعاؤها بشكل متزامن.

العدّاد السالب

Done هي Add(-1). إذا نزل العدد تحت الصفر، يسبّب البرنامج panic:

المخرجات recovered: sync: negative WaitGroup counter. السبب المعتاد goroutine فيها defer wg.Done() تستدعي أيضًا wg.Done() صراحة في أحد المسارات.

جمع الأخطاء

لا تفعل WaitGroup إلا العدّ. للأخطاء أعطِ كل goroutine خانتها الخاصة وافحصها بعد Wait:

تتخطّى errors.Join (Go 1.20) قيم nil وتعيد nil إذا كانت كلها nil، فتجمع "خطأ لكل goroutine" دون أي حسابات إضافية.

إذا أردت إيقاف العمل المتبقّي حالما تفشل goroutine واحدة، فاستخدم golang.org/x/sync/errgroup بدلًا من ذلك. إنها WaitGroup مع أول خطأ مع سياق يُلغى عند الفشل، وg.SetLimit(n) تحدّ التزامن. تعيش خارج المكتبة القياسية، فلا يمكن تشغيلها في محرّر هذه الصفحة:

g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
	g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
	return err // the first error; ctx was cancelled for the others
}

مجموعة عمّال

عدد ثابت من الـ goroutines يقرأ المهام من قناة يُبقي التزامن محدودًا مهما كان عدد المهام. تخبرك الـ WaitGroup متى انتهى كل العمّال، وهو الوقت الذي يمكن فيه إغلاق قناة النتائج.

ترتيب القطع الثلاث مهم:

  • يجب أن تستقبل main النتائج أثناء عمل العمّال. لو استدعت main الدالة wg.Wait() مباشرة قبل القراءة، لتوقّف العمّال عند الإرسال إلى results، ولما وصلوا أبدًا إلى Done، ولحدث جمود في كل شيء. لهذا تعمل Wait في goroutine خاصة بها.
  • لا يحدث close(results) إلا بعد Wait، فلا يستطيع أي عامل الإرسال على قناة مغلقة.
  • يعمل مغذّي المهام أيضًا في goroutine، فيتداخل التغذية والجمع.

أي عامل عالج أي مهمة يتغيّر من تشغيل إلى آخر، لذا يرتّب البرنامج حسب المهمة قبل الطباعة. وكل ما يطبعه حتمي.

WaitGroup أم قناة أم errgroup

الحاجةاستخدم
انتظار N goroutine، والنتائج في خانات مفهرسةsync.WaitGroup
انتظار goroutine واحدةقناة done أو قناة النتيجة نفسها
نتائج تتدفّق فور انتهائهاقناة تُغلق بعد wg.Wait()
إيقاف كل شيء عند أول خطأerrgroup.WithContext
إيقاف كل شيء عند انقضاء مهلة أو إلغاء من المستدعيcontext.Context مع WaitGroup أو errgroup

تضيف Go 1.25 الدالة wg.Go(func() { ... })، التي تجري Add(1) وDone المؤجّلة نيابة عنك. أما الشيفرة لـ Go 1.24 وما قبله، ومنها محرّر هذه الصفحة، فتستخدم الشكل الصريح المعروض سابقًا.

أخطاء شائعة

  • wg.Add(1) داخل الـ goroutine. قد تعود Wait قبل تنفيذها.
  • نسيان Done عند عودة مبكرة. استخدم دائمًا defer wg.Done().
  • تمرير الـ WaitGroup بالقيمة. استخدم مؤشرًا؛ ويبلغ go vet عن النسخ.
  • الانتظار في الـ goroutine نفسها التي يجب أن تفرّغ قناة. انقل wg.Wait() مع close إلى goroutine منفصلة.
  • إعادة استخدام WaitGroup قبل عودة Wait السابقة. ابدأ دورة جديدة من استدعاءات Add فقط بعد انتهاء Wait.

الأسئلة الشائعة

كيف تعمل sync.WaitGroup في Go؟

الـ WaitGroup عدّاد. تزيده wg.Add(n)، وتنقصه wg.Done() بواحد، وتتوقّف wg.Wait() حتى يصل إلى الصفر. استدعِ Add قبل تشغيل كل goroutine، وdefer wg.Done() داخلها، وWait حيث تحتاج أن ينتهي كل شيء.

هل أمرّر WaitGroup بالقيمة أم بمؤشر؟

بمؤشر (*sync.WaitGroup)، أو دع الـ goroutines تلتقطها في إغلاق. للنسخة عدّادها الخاص، فلا يصل Done على النسخة إلى الأصل أبدًا وتتوقّف Wait إلى الأبد. يبلغ go vet عن الخطأ برسالة "passes lock by value".

ما سبب رسالة "sync: negative WaitGroup counter"؟

استدعاءات Done أكثر من استدعاءات Add. عادة تستدعي goroutine الدالة Done مرتين (مرة مع defer ومرة صراحة)، أو يُتخطّى Add(1) في أحد المسارات. يسبّب البرنامج panic، لأن العدّاد لم يعد يخبرك بأي شيء صحيح.

كيف أحصل على الأخطاء من goroutines بدأتها مع WaitGroup؟

لا تحمل الـ WaitGroup نتائج ولا أخطاء. أعطِ كل goroutine خانتها الخاصة في شريحة أخطاء واجمعها بعد Wait (مثلًا بـ errors.Join)، أو استخدم golang.org/x/sync/errgroup، التي تعيد Wait فيها أول خطأ ويمكنها إلغاء الأخريات عبر سياق.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن