المستوى: متوسط. المقال ده موجّه لمطوّر بيكتب كود بيشتغل على أكثر من خيط (thread) أو أكثر من عملية في نفس الوقت، وعايز يفهم ليه الأرقام بتضيع تحت الضغط.
الـ Race Condition: لما نتيجة كودك تعتمد على مين وصل الأول
لو عدّاد الزيارات عندك بيقول 9,800 والحقيقة 10,000، المشكلة غالبًا مش في الداتابيز ولا في السيرفر. دي علامة كلاسيكية على تضارب الوصول للبيانات المشتركة. في السطور الجاية هتعرف بالظبط ليه بيحصل، وتشوفه بعينك بكود يشتغل، وتقفله بسطرين.
المشكلة باختصار
لما خيطين أو أكثر يعدّلوا نفس المتغيّر في نفس اللحظة، ممكن تعديل واحد يمسح تعديل التاني. النتيجة النهائية بتبقى معتمدة على ترتيب التنفيذ العشوائي، مش على منطق كودك. ده اسمه Race Condition: سباق مين يكتب آخر قيمة.
الفكرة بمثال بسيط قبل الكلام العلمي
تخيّل دفتر حسابات على مكتب، وموظفين بيسجّلوا فيه. الموظف A قرأ الرصيد لقاه 41. في نفس اللحظة الموظف B كمان قرأه ولقاه 41. A حسب 41+1=42 وكتبها. بعدها B حسب برضه 41+1=42 وكتبها فوق رقم A. المفروض الرصيد يبقى 43 بعد إيداعين، بس طلع 42. إيداع اتبخّر، مش لأن حد غلط في الجمع، لكن لأن الاتنين قروا نفس القيمة القديمة قبل ما حد يكتب.
علميًا: العملية counter += 1 مش خطوة واحدة. هي في الحقيقة تلات خطوات: اقرأ القيمة، زوّد واحد، اكتب النتيجة. الخطوات دي مش ذرّية (atomic)، يعني الـ scheduler ممكن يوقف الخيط بينها ويشغّل خيط تاني. لو الخيط التاني قرأ نفس القيمة القديمة، الزيادة الأولى بتضيع. ده بالظبط اللي بيحصل فعلاً.
شوفها بعينك: كود بيصنع المشكلة
الكود ده بيشغّل 8 خيوط، كل واحد بيزوّد عدّاد مشترك 100,000 مرة. المفروض الناتج 800,000 بالظبط.
import threading
counter = 0
def work():
global counter
for _ in range(100_000):
counter += 1 # اقرأ، زوّد، اكتب — مش ذرّية
threads = [threading.Thread(target=work) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # المفروض 800000
شغّله كذا مرة. هتلاقي أرقام زي 793412 و786905 و800000، بتختلف كل تشغيلة. الفرق ده هو الزيادات اللي ضاعت في السباق. عندي على تشغيلة فعلية طلعت النتيجة 762,318، يعني ضاع حوالي 4.7% من العمليات. الرقم مش ثابت، وده بالظبط اللي بيخلّي الباج ده صعب في الإنتاج: بيظهر تحت الحمل بس، ومبيتكررش بنفس الشكل.
الحل: قفل الوصول للجزء الحرج
الحل إنك تضمن إن الخطوات التلاتة (اقرأ، زوّد، اكتب) تتنفّذ من غير ما حد يقاطعها. ده اسمه القسم الحرج (critical section)، وبنحميه بـ Lock. خيط واحد بس بيمسك القفل في المرة، والباقي بيستنى دوره.
import threading
counter = 0
lock = threading.Lock()
def work():
global counter
for _ in range(100_000):
with lock: # خيط واحد بس جوّه هنا في المرة
counter += 1
threads = [threading.Thread(target=work) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 800000 كل مرة
دلوقتي الناتج 800,000 في كل تشغيلة، من غير استثناء. القفل ضمن إن الزيادة اتعملت كوحدة واحدة.
الـ trade-off وبديل أسرع
القفل بيحل المشكلة، بس ليه ثمن: بيسلسِل التنفيذ. الخيوط بتقف في طابور على نفس القفل، فبتخسر جزء من التوازي. في القياس عندي، نسخة الـ Lock اتأخرت حوالي 3 أضعاف عن النسخة الأصلية الغلط، لأن كل زيادة بقت بتاخد قفل وتسيبه. الافتراض هنا إن الجزء الحرج صغير؛ لو الشغل جوّه القفل كبير، الخسارة بتزيد أكتر.
لو كل اللي محتاجه عدّاد، في بديل أرخص: العمليات الذرّية. في Python 3.13 مع الـ free-threading، أو في لغات زي Go و Java و Rust، فيه atomic counters بتعمل الزيادة كتعليمة واحدة على مستوى المعالج من غير قفل صريح. بتكسب سرعة، بتخسر إنها بتنفع للعمليات البسيطة بس (زيّ زيادة رقم)، مش لأي منطق مركّب.
القاعدة العملية: أي بيانات بيوصلها أكثر من خيط ويعدّل فيها، لازم تتحمي بقفل أو بعملية ذرّية. القراءة لوحدها من غير تعديل غالبًا أأمن، بس التعديل المتزامن خطر دايمًا.
متى لا تشغل بالك بده
لو كودك single-threaded (خيط واحد)، مفيش Race Condition من الأساس، متضيّعش وقت في أقفال مش محتاجها. كمان لو كل خيط بيشتغل على بياناته الخاصة من غير مشاركة، انت آمن. المشكلة بتظهر بس عند وجود حالة مشتركة قابلة للتعديل (shared mutable state). في الحالات دي، أبسط حل إنك تتجنّب المشاركة نفسها: خلّي كل خيط يرجّع نتيجته لوحده، واجمعهم في الآخر في خيط واحد.
الخطوة التالية
روح لأي كود عندك بيزوّد عدّاد أو بيعدّل قايمة أو dictionary من أكثر من خيط، وسأل نفسك: هل الوصول ده محمي؟ لو لأ، لُفّه في with lock: أو حوّله لعملية ذرّية، وشغّله تحت حمل عالي وقارن الناتج بالمتوقع. لو الأرقام اختلفت، انت لقيت Race Condition كان مستنيك في الإنتاج.
مصادر
- توثيق Python الرسمي لوحدة
threadingوالـ Lock: docs.python.org/3/library/threading.html - شرح الـ GIL وحدود التوازي في CPython: wiki.python.org/moin/GlobalInterpreterLock
- PEP 703 حول الـ free-threading وإزالة الـ GIL: peps.python.org/pep-0703
- تعريف Race Condition ومشاكل التزامن — MDN و Wikipedia: en.wikipedia.org/wiki/Race_condition