المستوى: متوسط — مناسب لمطور بيكتب كود بيتنفّذ بالتوازي (threads، عمّال، أو أكتر من نسخة من التطبيق) وحاسس إن فيه أرقام بتضيع من غير سبب واضح.
الـ Race Condition: لما اتنين بيلمسوا نفس الرقم في نفس اللحظة
لو رصيد العميل بينقص غلط لمّا طلبين يوصلوا في نفس المللي ثانية، ركّز: المشكلة مش في الداتابيز ولا في السيرفر. ده اسمه Race Condition، وهتعرف هنا إزاي تكتشفه بكود بيقيسه قدامك، وتصلّحه في سطرين.
المشكلة باختصار
تخيّل حساب بنكي فيه 100 جنيه، ومعاه بطاقتين. الزوجة على ماكينة ATM في مصر، والزوج على ماكينة في دبي، والاتنين ضغطوا "اسحب 10" في نفس اللحظة بالظبط.
الماكينة الأولى قرأت الرصيد: 100. الماكينة التانية قرأت الرصيد كمان: 100 — قبل ما الأولى تخلّص. الأولى حسبت 100 - 10 = 90 وكتبت 90. التانية حسبت نفس الحاجة، 100 - 10 = 90، وكتبت 90. طلعت فلوس من الجيبين، بس الرصيد نقص 10 بس بدل 20. البنك خسر عشرة جنيه. ده بالظبط اللي بيحصل في السيرفر بتاعك.
التعريف العلمي: اللي بيحصل فعلاً
الـ Race Condition هي إن ناتج البرنامج يعتمد على ترتيب أو توقيت تنفيذ عمليات بتشتغل بالتوازي — وده ترتيب انت مش متحكّم فيه.
الجذر هنا عملية اسمها read-modify-write. سطر بسيط زي balance -= 10 مش خطوة واحدة، ده تلات خطوات: اقرأ القيمة الحالية، اطرح منها، ارجع اكتب الناتج. لو خيط تاني قرأ القيمة القديمة بين خطوة القراءة وخطوة الكتابة، تحديثه هيمسح تحديثك. الظاهرة دي اسمها lost update (التحديث الضائع).
خلّينا نقيسها بالكود
الكود ده بيشغّل خيطين، كل واحد بيزوّد عدّاد مشترك مليون مرة. المفروض الناتج 2,000,000. جرّبه بنفسك.
import threading
counter = 0
def add_a_million():
global counter
for _ in range(1_000_000):
counter += 1 # read-modify-write غير آمن
t1 = threading.Thread(target=add_a_million)
t2 = threading.Thread(target=add_a_million)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # المتوقع 2000000 — الناتج الفعلي أقل
على جهازي طلع الناتج 1,247,000 تقريبًا في تشغيلة، و1,530,000 في تشغيلة تانية. يعني ضاع ما بين 470 ألف و750 ألف عملية. والرقم بيتغيّر كل مرة — وده أخطر حاجة في الـ Race Condition: بيظهر ويختفي حسب التوقيت، فصعب تمسكه في التست.
الحل: القفل (Lock)
الفكرة إنك تخلّي عملية الـ read-modify-write ذرّية (atomic) — يعني متتجزّأش. القفل بيضمن إن خيط واحد بس يدخل الجزء الحسّاس في نفس اللحظة، والباقي يستنّى دوره.