المستوى: متوسط. المقال ده موجّه لمطوّر بيكتب كود بيشتغل على أكثر من خيط (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. خيط واحد بس بيمسك القفل في المرة، والباقي بيستنى دوره.