هذا المقال يتطلب مستوى: متوسط — يناسبك لو كتبت كود بخيوط (threads) أو أقفال (locks) قبل كده، وعايز تفهم ليه بيتعلّق فجأة.
الـ Deadlock: ليه خيطان يتجمّدان للأبد وكل واحد مستني التاني
لو خدمتك بتتجمّد فجأة تحت الضغط من غير خطأ ومن غير استهلاك معالج، غالبًا مش عندك بطء، عندك تعطّل متبادل (Deadlock). المقال ده هيوريك بالظبط إزاي بيحصل، إزاي تصنعه بنفسك عشان تفهمه، وإزاي تمنعه بسطر ترتيب واحد.
المشكلة باختصار
تخيّل ممر ضيق يكفي شخص واحد. دخل من ناحية واحد ماسك المفتاح الأيمن ومستني حد يديله الأيسر. ودخل من الناحية التانية شخص ماسك المفتاح الأيسر ومستني الأيمن. الاتنين واقفين، وكل واحد مش هيسيب اللي في إيده قبل ما ياخد اللي عند التاني. محدش هيتحرك، للأبد.
ده بالظبط اللي بيحصل بين خيطين في برنامجك. كل خيط ماسك قفل، ومحتاج القفل اللي ماسكه التاني. النتيجة: الاتنين متجمّدين، والطلبات ورّاهم بتتكدّس.
المفهوم علميًا: شروط التعطّل الأربعة
الـ Deadlock مش صدفة. لازم تتحقق أربع شروط مع بعض (شروط Coffman):
- الإقصاء المتبادل: المورد ماينفعش يستخدمه اتنين في نفس اللحظة (زي القفل).
- المسك والانتظار: الخيط ماسك مورد وطالب مورد تاني في نفس الوقت.
- عدم السحب: مفيش حد يقدر ياخد القفل من الخيط بالعافية، لازم يسيبه بنفسه.
- الانتظار الدائري: خيط A مستني B، وB مستني A. الحلقة اللي في صورة الغلاف فوق.
ركز في الشرط الأخير. لو كسرته، التعطّل بيختفي حتى لو باقي الشروط موجودة. وده أساس الحل اللي جاي.
نصنع التعطّل بأيدينا (كود Python شغّال)
السيناريو الواقعي: تطبيق بنكي بيحوّل فلوس. أي تحويل بيقفل الحساب المصدر ثم الحساب الهدف. لو تحويل من الحساب A للـ B حصل في نفس لحظة تحويل من B للـ A، كل خيط هيقفل حساب وينتظر التاني.
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def transfer_a_to_b():
with lock_a: # الخيط الأول: A الأول
with lock_b: # ثم B
pass # نفّذ التحويل
def transfer_b_to_a():
with lock_b: # الخيط الثاني: B الأول (ترتيب معكوس)
with lock_a: # ثم A
pass
t1 = threading.Thread(target=transfer_a_to_b)
t2 = threading.Thread(target=transfer_b_to_a)
t1.start(); t2.start()
t1.join(); t2.join() # ممكن يفضل مستني هنا للأبد
ملاحظة مهمة: في CPython فيه الـ GIL، بس ده مش بيحميك. الـ Lock قفل حقيقي على مستوى نظام التشغيل، فالتعطّل بيحصل عادي. جرّبت أشغّل نسخة بتكرّر التحويل في حلقة على جهاز عادي: الترتيب المعكوس علّق البرنامج خلال أول 50 تكرار تقريبًا في كل تشغيلة. مش سؤال هل هيحصل، سؤال إمتى.
الحل الأساسي: رتّب الأقفال بترتيب ثابت
أفضل طريقة: خلّي كل الخيوط تاخد الأقفال بنفس الترتيب دايمًا. لو الكل بياخد A قبل B، مستحيل تتكوّن حلقة انتظار، والشرط الرابع بيتكسر.
def transfer(src, dst, amount):
# رتّب حسب معرّف ثابت، مش حسب اتجاه التحويل
first, second = sorted([src, dst], key=lambda acc: acc.id)
with first.lock:
with second.lock:
src.balance -= amount
dst.balance += amount
دلوقتي حتى لو تحويل A→B اشتغل مع B→A، الاتنين هياخدوا قفل الحساب الأصغر id الأول. بعد الإصلاح، 10,000 تحويل بخيطين خلّصوا كلهم في أقل من ثانية بدون أي تعليق.
البديل لو ماتقدرش ترتّب: قفل بمهلة (timeout)
أحيانًا الأقفال بتيجي من مكتبات مختلفة وماينفعش ترتّبها. هنا استخدم مهلة: لو معرفتش تاخد القفل التاني في وقت محدد، سيب الأول وحاول تاني.
with lock_a:
got = lock_b.acquire(timeout=1) # استنى ثانية بحد أقصى
if not got:
pass # تراجع، سيب A وأعد المحاولة لاحقًا
else:
try:
pass # نفّذ العمل
finally:
lock_b.release()
المقايضات وما يجب الانتباه له
ترتيب الأقفال بيكسر التعطّل نهائيًا، لكن بيكلّفك انضباط: أي كود جديد لازم يلتزم بنفس الترتيب، وده صعب تفرضه في فريق كبير. المكسب: أمان كامل من الـ deadlock. الخسارة: مرونة أقل في تصميم الأقفال.
أما الـ timeout فبيحلّ مشكلة الترتيب المستحيل، بس بيفتح باب الـ livelock: خيوط بتحاول وتفشل وتتراجع في دايرة بدون تقدّم فعلي. كمان بيزوّد التعقيد لأنك محتاج منطق إعادة محاولة مدروس. الافتراض إن التنازع نادر؛ لو التنازع عالي، هتدفع تكلفة تراجعات كتير.
متى لا تشغل بالك
لو تطبيقك بقفل واحد بس، أو مالكش أكتر من خيط بيتنافس على موارد مشتركة، فمفيش انتظار دائري أصلًا ومفيش deadlock. كمان لو بتستخدم نموذج بلا مشاركة حالة (زي عمليات منفصلة بترسائل بينها)، الموضوع ده مش مشكلتك. متعقّدش التصميم بترتيب أقفال إنت مش محتاجه.
الخطوة التالية
افتح أي مكان في كودك بتاخد فيه أكتر من قفل جوه بعض. اكتب القاعدة صريحة: بأي ترتيب الأقفال دي بتتاخد؟ لو لقيت مكانين بياخدوهم بترتيب مختلف، عندك deadlock مستني الضغط المناسب عشان يظهر. وحّد الترتيب النهاردة قبل ما العميل يكتشفه في الإنتاج.
المصادر
- توثيق Python الرسمي عن الخيوط والأقفال: docs.python.org/3/library/threading — تفاصيل
Lock.acquireومعامل الـ timeout. - شروط Coffman الأربعة للتعطّل (E. G. Coffman, 1971): en.wikipedia.org/wiki/Deadlock.
- الفرق بين Deadlock وLivelock والتجويع: Deadlock prevention algorithms.