مستوى المقال: متوسط. الافتراض إنك تعرف يعني إيه thread وقفل (lock) أساسي. لو لسه جديد على الـ threads، المثال الأول في المقال هيوصّلك الفكرة قبل الكود.
في آخر المقال هتعرف تكتشف جمود (deadlock) بيجمّد خدمتك من غير ما يطلع ولا رسالة خطأ، وتصلحه بقاعدة واحدة بسيطة. الوقت المتوقع للقراءة حوالي 7 دقايق.
الجمود (Deadlock): لما خيطان يستنّيا بعض للأبد
ركز في الفرق ده: البرنامج اللي بيقع بيديك رسالة خطأ وستاك تريس. البرنامج اللي فيه جمود مش بيقع، بس مش بيتحرك. بيفضل واقف مستني حاجة مش هتيجي أبدًا. ده أخطر، لأنه بيعدي في التستات وبيموت في الإنتاج.
المشكلة باختصار
تخيّل أخوين في مطبخ. الأول ماسك السكينة ومستني اللوح عشان يقطّع. التاني ماسك اللوح ومستني السكينة. كل واحد مش هيسيب اللي في إيده قبل ما ياخد اللي ناقصه. النتيجة: الاتنين واقفين، والأكل مش هيتعمل. مفيش حد بيصرخ، بس المطبخ متجمّد.
ده بالظبط اللي بيحصل بين الخيوط. القفل هو السكينة أو اللوح. الخيط اللي ماسك قفل ومستني قفل تاني ماسكه خيط بيستنى قفلك انت — ده انتظار دائري (circular wait)، وهو قلب الجمود.
الشرح العلمي: شروط Coffman الأربعة
الجمود مش بيحصل صدفة. في 1971 حدّد Coffman وزملاؤه أربع شروط لازم تتحقق كلها مع بعض عشان يحصل جمود. لو كسرت أي واحدة منهم، الجمود مستحيل:
- الإقصاء المتبادل (Mutual Exclusion): المورد مينفعش يتمسك بأكتر من خيط في نفس الوقت. القفل بطبيعته كده.
- المسك والانتظار (Hold and Wait): الخيط ماسك مورد وفي نفس الوقت مستني مورد تاني.
- عدم النزع (No Preemption): مفيش حد يقدر ياخد القفل من الخيط بالعافية، لازم يسيبه بنفسه.
- الانتظار الدائري (Circular Wait): سلسلة خيوط كل واحد مستني اللي بعده، وآخر واحد مستني الأول.
الحل العملي بيهاجم الشرط الرابع، لأنه أسهل واحد تكسره من غير ما تخرّب أداء برنامجك.
نعمله بأيدينا: كود يوريك الجمود
الكود ده بيعمل خيطين، كل واحد بياخد قفلين بترتيب معكوس. حطّينا sleep صغير عشان نضمن إن الجمود يحصل في كل مرة تشغّل:
import threading, time
lock_a = threading.Lock()
lock_b = threading.Lock()
def worker_1():
with lock_a:
time.sleep(0.1)
with lock_b:
pass
def worker_2():
with lock_b:
time.sleep(0.1)
with lock_a:
pass
t1 = threading.Thread(target=worker_1)
t2 = threading.Thread(target=worker_2)
t1.start(); t2.start()
t1.join(timeout=3)
t2.join(timeout=3)
if t1.is_alive() or t2.is_alive():
print("deadlock happened")
else:
print("done")
لو شغّلته، هيطبع رسالة الجمود. من غير الـ timeout في join كان البرنامج هيقف للأبد. لاحظ: مفيش استثناء، مفيش خطأ. البرنامج شكله شغّال، بس واقف.
الحل: ترتيب أقفال عام (Lock Ordering)
القاعدة بسيطة: خلّي كل الخيوط تاخد الأقفال بنفس الترتيب دائمًا. لو الكل بياخد lock_a قبل lock_b، مستحيل يحصل انتظار دائري.
def worker(first, second):
with first:
time.sleep(0.1)
with second:
pass
t1 = threading.Thread(target=worker, args=(lock_a, lock_b))
t2 = threading.Thread(target=worker, args=(lock_a, lock_b))
t1.start(); t2.start()
t1.join(); t2.join()
print("no deadlock")
الفرق بالأرقام: النسخة الأولى بتقف عند 3 ثواني (وكانت للأبد). النسخة التانية بتخلّص شغلها الفعلي في أقل من ميلي ثانية بعد الـ sleep. نفس المنطق، نفس الأقفال، بس الترتيب لوحده هو الفرق بين خدمة شغّالة وخدمة ميتة.
سيناريو واقعي: تحويل فلوس بين حسابين
ده مش مثال أكاديمي. تخيّل خدمة تحويل بتخدم 50 ألف عملية في اليوم. كل تحويل بيقفل الحسابين المشتركين. لو عميل بيحوّل من حساب 1 لحساب 2، وفي نفس اللحظة عميل تاني بيحوّل من 2 لـ 1، هيحصل جمود. الخيطين بيتجمدوا، وبعد شوية الـ thread pool بيخلص خيوطه الحرة، وبعدها الخدمة كلها بتقف — مش التحويلين بس. الحل: رتّب القفل حسب رقم الحساب دائمًا:
def transfer(from_acc, to_acc, amount):
first, second = sorted([from_acc, to_acc], key=lambda a: a.id)
with first.lock:
with second.lock:
from_acc.balance -= amount
to_acc.balance += amount
دلوقتي الاتنين بياخدوا القفل بنفس الترتيب مهما كان اتجاه التحويل. الانتظار الدائري اتكسر.
الـ trade-offs وحاجات تنتبه لها
- بتكسب: ضمان عدم حصول جمود، بصفر تكلفة أداء وقت التشغيل.
- بتخسر: انضباط. كل مكان في الكود بياخد قفلين لازم يحترم نفس الترتيب. لو مبرمج نسي وعكس الترتيب، رجع الجمود.
بديل تاني هو acquire(timeout=1): لو مقدرتش تاخد القفل في ثانية، سيب اللي معاك وحاول تاني. ده بيكسر شرط المسك والانتظار، لكن بتكلفة زمن استجابة أعلى وخطر livelock (الخيوط تفضل تحاول وتتراجع من غير ما تخلّص). الافتراض هنا إن عندك عدد أقفال محدود ومعروف.
متى لا تستخدم هذا
لو خيطك بياخد قفل واحد بس ومش بيعشش أقفال جوه بعضها، الجمود مستحيل أصلًا — متعقّدش الكود بترتيب مالوش لازمة. وكمان لو الداتا بتاعتك صغيرة أو ثابتة (immutable)، فكّر في قفل عام واحد بسيط أو بنية lock-free بدل ما تدير أقفال متعددة.
الخطوة التالية
افتح الكود بتاعك ودوّر على أي مكان بتاخد فيه قفلين مع بعض (with lock1: وجواها with lock2:). اكتب قاعدة ترتيب واحدة مكتوبة (مثلًا: بالاسم أو بالـ id) والتزم بيها في كل المسارات. لو لقيت مكان بياخد الأقفال بترتيب مختلف، ده بالظبط اللي هيجمّدلك السيرفر يوم ما الحِمل يعلى.
المصادر
- توثيق بايثون الرسمي — threading والـ Lock: docs.python.org/3/library/threading.html
- E. G. Coffman, M. Elphick, A. Shoshani — System Deadlocks, ACM Computing Surveys, 1971 (الشروط الأربعة للجمود).
- Wikipedia — Deadlock: en.wikipedia.org/wiki/Deadlock
- Wikipedia — Dining philosophers problem: en.wikipedia.org/wiki/Dining_philosophers_problem
- Oracle Java Tutorials — Deadlock: docs.oracle.com/javase/tutorial/essential/concurrency/deadlock.html