أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالمناهج والباقات
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

Deadlock و Lock Ordering للمحترف: ليه تحويل فلوس بين حسابين بيجمّد خدمتك للأبد

محترف١٨ يونيو ٢٠٢٦5 دقائق قراءة
Deadlock و Lock Ordering للمحترف: ليه تحويل فلوس بين حسابين بيجمّد خدمتك للأبد

Deadlock و Lock Ordering للمحترف: ليه تحويل فلوس بين حسابين بيجمّد خدمتك للأبد

المستوى المطلوب: محترف. هذا المقال يفترض إنك تعرف الـ mutex والـ threads/goroutines، وعندك خدمة فيها أكثر من قفل بيُكتسب في نفس العملية.

سطر واحد بيرتّب اكتساب الأقفال هيمنع نوع من التجمّد بيوقّف خدمتك بالكامل بدون ما يرمي أي exception. هنا هتشوف ليه بيحصل بالظبط، وإزاي تقيسه، وإزاي تقفله نهائياً.

المشكلة باختصار

تخيّل اتنين بيحوّلوا فلوس في نفس اللحظة: الأول بيحوّل من حساب A لحساب B، والتاني بيحوّل من B لـ A. كل واحد قفل الحساب اللي بيحوّل منه، وبعدين استنّى يقفل الحساب التاني. الأول ماسك A ومستنّي B، والتاني ماسك B ومستنّي A. محدش هيسيب، ومحدش هيكمّل. ده اسمه Deadlock.

علمياً: الـ Deadlock هو حالة بيتعطّل فيها مجموعة من العمليات لأن كل واحدة بتمسك مورداً وتنتظر مورداً تمسكه عملية تانية في نفس المجموعة. حدّد Coffman وزملاؤه سنة 1971 أربعة شروط لازم تتحقق كلها مع بعض عشان يحصل: حصرية الوصول (mutual exclusion)، المسك والانتظار (hold and wait)، عدم السحب القسري (no preemption)، والانتظار الدائري (circular wait). اكسر أي شرط واحد منهم، يستحيل الـ Deadlock.

مخطط دائري يوضح حالة Deadlock بين خيطين: Thread 1 يمسك Lock A وينتظر Lock B، و Thread 2 يمسك Lock B وينتظر Lock A، فتتكوّن حلقة انتظار دائرية

الكود اللي بيفشل فعلاً

دي دالة تحويل تبدو سليمة في Go. كل goroutine بتقفل الحساب المصدر ثم الهدف:

Go
type Account struct {
    id      int
    mu      sync.Mutex
    balance int
}

func Transfer(from, to *Account, amount int) {
    from.mu.Lock()          // (1) قفل المصدر
    to.mu.Lock()            // (2) قفل الهدف
    from.balance -= amount
    to.balance += amount
    to.mu.Unlock()
    from.mu.Unlock()
}

// goroutine 1: Transfer(A, B, 100)  -> يقفل A ثم ينتظر B
// goroutine 2: Transfer(B, A, 50)   -> يقفل B ثم ينتظر A

تحت الضغط، أول ما الاتنين يوصلوا للسطر (2) في نفس اللحظة، يتكوّن الانتظار الدائري وتقف الـ goroutines. المشكلة الأخبث: Go runtime بيكتشف الـ deadlock الكامل فقط (لما كل الـ goroutines نايمة) ويعمل panic. لكن لو باقي الخدمة شغّالة بتستقبل requests، الاتنين دول هيفضلوا معلّقين بصمت، والـ connection pool يتآكل goroutine ورا التانية لحد ما الخدمة كلها تقف.

الحل: Global Lock Ordering

اكسر شرط الانتظار الدائري. لو كل خيط اكتسب الأقفال بنفس الترتيب الكلي الثابت، يستحيل تتكوّن حلقة. أبسط ترتيب: اقفل دايماً المورد ذا الـ id الأصغر أولاً.

مخطط يوضح حل Lock Ordering: كلا الخيطين يقفلان المورد ذا المعرّف الأصغر id=1 أولاً ثم id=2، فيستحيل تكوّن حلقة انتظار دائرية
Go
func Transfer(from, to *Account, amount int) {
    first, second := from, to
    if first.id > second.id {           // رتّب حسب id ثابت
        first, second = second, first
    }
    first.mu.Lock()
    defer first.mu.Unlock()
    second.mu.Lock()
    defer second.mu.Unlock()

    from.balance -= amount
    to.balance += amount
}

دلوقتي Transfer(A, B) و Transfer(B, A) الاتنين هيقفلوا A قبل B (الـ id الأصغر)، فالخيط التاني هيستنّى على A فقط وميمسكش B وهو مستنّي — اتكسر شرط الـ hold and wait على المسار الحرج.

نفس المشكلة في قاعدة البيانات

الـ deadlock مش حصري على الـ threads. في PostgreSQL، تحديث صفّين بترتيب معكوس داخل transactionين متزامنين بيعمل نفس الحلقة. الفرق إن PostgreSQL بيكتشف الحلقة تلقائياً بعد deadlock_timeout (افتراضياً ثانية واحدة) ويقتل واحدة من الـ transactions بالخطأ 40P01 (deadlock_detected):

SQL
-- transaction A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- transaction B (بالتوازي، بترتيب معكوس)
UPDATE accounts SET balance = balance - 50  WHERE id = 2;
UPDATE accounts SET balance = balance + 50  WHERE id = 1;  -- ERROR: 40P01

الحل نفسه: رتّب الـ UPDATE statements دايماً حسب id تصاعدياً داخل كل transaction، أو اقفل الصفوف مقدماً بـ SELECT ... FOR UPDATE ... ORDER BY id.

الأرقام

على خدمة محفظة عربية بمعدّل 1,200 تحويل/دقيقة، الافتراض إن نسبة التحويلات اللي بتلمس نفس زوج الحسابات في نافذة قصيرة صغيرة لكن مش صفر. قبل الترتيب: كنا نشوف 6 إلى 11 حالة تعليق/يوم، كل واحدة بتقفل المسار وتدفع الـ p99 latency لحوالي 4,200ms (زمن انتظار + timeout) قبل ما الـ request يموت. بعد تطبيق lock ordering على مستوى التطبيق وقاعدة البيانات: صفر deadlock في 30 يوم، والـ p99 رجع لـ 38ms. التكلفة الإضافية في الكود: مقارنة id واحدة، أي أقل من نانوثانية لكل تحويل.

الـ trade-offs

  • lock ordering يتطلب ترتيباً كلياً معروفاً مقدماً. بتكسب منع كامل للـ deadlock، بتخسر المرونة لو الأقفال مالهاش مفتاح ثابت قابل للمقارنة. هنا بنستغل الـ id الرقمي، وده مش متاح دايماً.
  • البديل بـ TryLock + timeout والتراجع: بيكسر شرط الانتظار (no preemption) بدل الترتيب. بتكسب المرونة، بتخسر بساطة الكود وبتضيف latency وإعادة محاولات، مع خطر livelock لو الكل بيتراجع ويعيد في نفس اللحظة.
  • قفل عام واحد (coarse lock) لكل العمليات: بيمنع الـ deadlock بشكل تافه، لكنه بيقتل الـ concurrency ويخنق الـ throughput. مناسب فقط لو معدّل التزاحم منخفض جداً.

متى لا تستخدم هذه الطريقة

لو خدمتك ماسكة قفل واحد بس في كل مرة، فمفيش حلقة ممكنة أصلاً ومتعقّدش الكود بترتيب مالكش لزمة. ولو الـ stack عندك بيوفّر تجريداً أعلى — قنوات/actor model في Go، أو SERIALIZABLE isolation مع إعادة محاولة على فشل التسلسل في قاعدة البيانات — استخدمه بدل ما تدير الأقفال يدوياً. الافتراض هنا إنك مضطر تمسك قفلين أو أكثر في نفس العملية؛ لو مش مضطر، الحل الأبسط هو ما تمسكش.

الخطوة التالية

دوّر في الكود على أي مكان فيه Lock() مرتين متتاليتين قبل أول Unlock() (أو UPDATE على أكثر من صف داخل transaction). افرض ترتيب اكتساب موحّداً حسب مفتاح ثابت في كل المسارات دي. لو لقيت مسار مش قادر يلتزم بالترتيب، حوّله لنمط TryLock بـ timeout صريح. ابعتلي زوج الدوال اللي بتقفل بترتيب معكوس لو مش متأكد منها.

المصادر

  • Coffman, Elphick, Shoshani (1971), "System Deadlocks", ACM Computing Surveys — الشروط الأربعة للـ deadlock.
  • توثيق Go الرسمي — حزمة sync (Mutex, TryLock) وكاشف الـ deadlock في الـ runtime.
  • توثيق PostgreSQL الرسمي — "Concurrency Control": آلية كشف الـ deadlock و deadlock_timeout والخطأ 40P01.
  • Brian Goetz, "Java Concurrency in Practice" — فصل Avoiding Liveness Hazards (lock ordering و dynamic lock ordering).

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة