مستوى المقال: متوسط — يفترض إنك تعرف Python أو لغة شبيهة، فاهم threads على المستوى النظري، وعندك تطبيق ويب بيتعامل مع قاعدة بيانات.
Race Conditions: ليه نفس الكود بينجح في التيست ويفشل في الإنتاج
لو endpoint الشراء بتاعك خصم آخر قطعة من المخزن واتفاجأت إن اتنين عملاء استلموا نفس القطعة، الكود مش غلط. اللي حصل إن خيطين تنفيذ قروا الرقم في نفس النانوثانية وكتبوا كل واحد قراره. ده اسمه Race Condition، وبيظهر تحت الضغط لما الـ unit tests الفردية بتعدّيه بدون ما تلاحظه.
سيناريو واقعي قبل التعريف العلمي
تخيل محل بيبيع آخر تذكرة لحفلة. وقف شخصين أمام الكاشير في نفس اللحظة. الموظف بصّ في النظام ولقى "متبقي 1". قال للأول "آه عندنا"، وفي نفس الثانية قال للتاني "آه عندنا". الاتنين دفعوا. النظام نفّذ 1 - 1 = 0 ثم 1 - 1 = 0 تاني، لأن كل عملية شافت الرقم الأصلي. النتيجة: تذكرة واحدة، عميلان غاضبان.
ده بالظبط اللي بيحصل في endpoint الشراء لما خيطين بيشتغلوا بالتوازي على نفس الصف في قاعدة البيانات. كل خيط بيقرا stock = 1، بيحسب stock - 1 = 0، وبيكتب 0. الناتج: قطعة واحدة باعت لاتنين.
التعريف العلمي بالظبط
الـ Race Condition بيحصل لما النتيجة النهائية لعمليتين بتعتمد على ترتيب وصولهم لمورد مشترك بدون تنسيق. المنطقة من الكود اللي فيها قراءة-تعديل-كتابة على بيانات مشتركة اسمها Critical Section. لو محصلش Mutual Exclusion على الـ critical section، النتيجة بتبقى غير محددة (non-deterministic) وبتختلف من تشغيلة للتانية.
المصطلحات المرتبطة في علم الـ concurrency:
- Critical Section: الكود اللي بيلمس مورد مشترك (متغير، صف DB، ملف).
- Mutual Exclusion (Mutex): ضمان إن خيط واحد فقط داخل الـ critical section في نفس اللحظة.
- Atomic Operation: عملية بتتنفذ كوحدة واحدة غير قابلة للتجزئة، ما ينفعش حاجة تخش بينها.
- Data Race: حالة خاصة لما خيطين على الأقل بيوصلوا لنفس الذاكرة وواحد منهم على الأقل بيكتب، بدون synchronization.
كود يعيد إنتاج المشكلة في 30 ثانية
import threading
balance = 1000 # حساب فيه 1000 جنيه
def withdraw(amount):
global balance
current = balance # 1) قراءة
new_balance = current - amount # 2) حساب
balance = new_balance # 3) كتابة
threads = [threading.Thread(target=withdraw, args=(100,)) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"المتبقي: {balance}")
# المتوقع: 0
# الفعلي على معظم التشغيلات: 100 أو 200 أو 300