تخطَّ إلى المحتوى

٢١ يوليو ٢٠٢٦

حل خطأ fatal error: all goroutines are asleep - deadlock! في Go

شارك المقال:

Go runtime يراقب كل الـ goroutines بالبرنامج، ولو لاحظ أن كل واحدة منها متوقّفة بانتظار شيء لن يحدث أبدًا، يوقف البرنامج فورًا بدل تعليقه للأبد:

fatal error: all goroutines are asleep - deadlock!

هذه ليست panic عادية تلتقطها بـ recover — هي قرار من الـ runtime نفسه بإيقاف كل شيء لأنه لا يرى أي أمل بالتقدّم.

السبب الجذري دائمًا واحد

كل الـ goroutines — بما فيها main — عالقة بانتظار عملية قناة أو قفل مزامنة لن يُكمله أي طرف آخر. أشهر ثلاث صور لهذا:

1) قناة بلا مستقبل (أو بلا مرسل)

القناة غير المخزَّنة (unbuffered) تحجب المرسل حتى يستقبل طرف آخر القيمة مباشرة. لو ما في أحد يستقبل، الإرسال يعلّق للأبد:

func main() {
  ch := make(chan int)
  ch <- 42          // ينتظر مستقبِلًا... لا يوجد أحد
  fmt.Println("لن نصل هنا أبدًا")
}

الحل: شغّل الاستقبال بـ goroutine منفصلة، أو استخدم قناة مخزَّنة إن كنت تعرف عدد القيم مسبقًا:

ch := make(chan int, 1)   // مخزَّنة بسعة 1 — لا تحتاج مستقبِلًا فوريًا
ch <- 42
fmt.Println(<-ch)

2) انتظار بلا goroutine ثانية أصلًا

استقبال من قناة داخل نفس الـ goroutine الوحيدة بالبرنامج بلا أي مصدر آخر يملأها:

func main() {
  ch := make(chan string)
  msg := <-ch        // ننتظر قيمة... ما في غيرنا ليرسلها
  fmt.Println(msg)
}

الحل: تأكّد أن كل قناة تنتظر منها قيمة لها مصدر فعلي — عادة go تشغّل الدالة التي ترسل:

ch := make(chan string)
go func() {
  ch <- "جاهز"
}()
fmt.Println(<-ch)

3) مزامنة لن تكتمل — WaitGroup أو Mutex

sync.WaitGroup ينتظر wg.Done() بعدد يساوي wg.Add() بالضبط. لو نُسي Done بأحد المسارات (مثلًا داخل شرط أو مسار خطأ)، wg.Wait() ينتظر للأبد:

var wg sync.WaitGroup
wg.Add(1)
go func() {
  // defer wg.Done() نُسيت هنا — الانتظار لن ينتهي أبدًا
  doWork()
}()
wg.Wait()

الحل: ضع defer wg.Done() أول سطر داخل الـ goroutine مباشرة، قبل أي منطق قد يتفرّع أو يفشل مبكرًا:

wg.Add(1)
go func() {
  defer wg.Done()   // يُنفَّذ دائمًا مهما كان مسار الخروج
  doWork()
}()
wg.Wait()

نفس المبدأ ينطبق على mu.Lock() بلا mu.Unlock() مقابل بمسار خروج معيّن — القفل يبقى محجوزًا وأي goroutine أخرى تنتظره تتجمّد.

لماذا يظهر أحيانًا فقط؟

لأن الجمود يعتمد على ترتيب تشغيل الـ goroutines، وGo لا يضمن نفس الترتيب بكل تشغيلة. قد يعمل كودك بالصدفة عدّة مرّات قبل أن يظهر الخطأ — وهذا بالضبط ما يجعله خطيرًا بالإنتاج لو اختبرته بسرعة فقط محليًا.

خطوات التشخيص السريعة

  1. لكل ch <- v أو <-ch بالكود، اسأل: من الطرف الآخر فعليًا وهل هو يعمل بالتأكيد؟
  2. لكل wg.Add(n)، تأكّد أن wg.Done() يُنفَّذ بالضبط n مرّة مهما كان مسار الخروج — defer أول الدالة أضمن مكان.
  3. شغّل go run -race main.go — يكشف أيضًا سباقات البيانات المرتبطة غالبًا بنفس الكود المعطوب.

الخلاصة

all goroutines are asleep - deadlock! رسالة واضحة النية: كل شيء متوقّف بانتظار طرف لن يصل. تتبّع كل قناة ومزامنة بالكود واسأل من المفترض يحرّكها من الطرف الآخر — الحل يكون قريبًا دائمًا.

تعمّق أكثر بدرسي القنوات (Channels) والمزامنة بـ sync ضمن مسار Go الكامل بالعربي.

📚 مصادر رسمية للتعمّق: التوثيق الرسمي لـ Go

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

هل هذا الخطأ panic عادي يمكن التقاطه بـ recover؟

لا. fatal error بسبب deadlock يكتشفه Go runtime نفسه لا كودك، فيوقف البرنامج مباشرة قبل أن تصل لأي defer أو recover. الحل الوحيد هو إصلاح سبب الجمود بالكود، لا محاولة التقاطه وقت التشغيل.

لماذا لا يظهر الخطأ إلا أحيانًا؟

لأن الجمود يعتمد على ترتيب تنفيذ الـ goroutines، وGo runtime يجدولها بترتيب غير ثابت تمامًا. أحيانًا goroutine أخرى تصل للقناة بالصدفة قبل أن يكتشف الـ runtime أن الجميع متوقّف، فيبدو الكود يعمل أحيانًا ويتجمّد أحيانًا أخرى — هذا بالضبط سبب خطورته.

ما الفرق بين هذا الخطأ وrace condition؟

deadlock توقّف كامل: كل الـ goroutines عالقة ولا شيء يتقدّم، والبرنامج ينهار فورًا برسالة واضحة. أما race condition فالبرنامج يستمر بالعمل لكن بنتائج غير متوقّعة لأن أكثر من goroutine تعدّل نفس البيانات بلا حماية — أخطر لأنه لا يوقف البرنامج ولا يظهر برسالة صريحة.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.