المستوى المطلوب: مبتدئ. هنبدأ بمثال من الحياة، وبعدين نفكّك المفهوم علميًا. كل الكود في المقال قابل للنسخ والتشغيل على Python عادي.
لو خيطين (threads) في برنامجك بيزوّدوا نفس الرقم في نفس اللحظة، البرنامج ممكن يحسبلك زيادة واحدة بدل اتنين — من غير أي رسالة خطأ ومن غير ما الكود يقع. ده اسمه Race Condition، والمقال ده هيوريك بالظبط ليه بيحصل وإزاي تمنعه في سطر واحد.
Race Condition: لما خيطين يتسابقوا على نفس القيمة
المثال الأول: درج الكاشير
تخيّل محل فيه درج فلوس واحد، وكاشيرين بيشتغلوا عليه. كل واحد فيهم عايز يضيف 10 جنيه. الطريقة الصح: تفتح الدرج، تشوف فيه كام، تزوّد 10، تقفله.
دلوقتي بصّ على اللي بيحصل فعلاً لما يتحركوا في نفس اللحظة: الكاشير الأول فتح الدرج ولقى 100. في نفس اللحظة الكاشير التاني فتح ولقى 100 برضه. الأول حسب 110 وكتبها. التاني — اللي لسه شايف 100 — حسب 110 وكتبها. الدرج بقى فيه 110، رغم إن المفروض يكون 120. عشرة جنيه اختفوا، ومفيش حد عمل غلط واضح.
المشكلة باختصار
الكاشير هنا = الخيط (thread). الدرج = متغيّر مشترك في الذاكرة. عملية "زوّد واحد" مش خطوة واحدة، هي تلات خطوات: اقرأ القيمة، عدّل عليها، اكتبها. الاسم العلمي للنمط ده هو read-modify-write. المشكلة إن الخطوات التلاتة دي مش ذرّية (atomic)، يعني خيط تاني يقدر يدخل بينها ويقرا قيمة قديمة قبل ما الأول يخلّص كتابة. لما ده يحصل، زيادة بتضيع.
نوريك المشكلة بكود حقيقي
الكود ده بيشغّل 4 خيوط، كل واحد بيزوّد عدّاد مشترك 200 ألف مرة. المفروض الناتج يكون 800 ألف بالظبط:
import threading
counter = 0
def work():
global counter
for _ in range(200000):
local = counter # 1) اقرأ القيمة الحالية
counter = local + 1 # 2) اكتبها زائد واحد
threads = [threading.Thread(target=work) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # المفروض 800000... بس بيطلع أقل بكتير
في تشغيل فعلي عندنا على Python 3.11 بـ 4 خيوط، الناتج طلع 364,422 بدل 800,000. يعني ضاع 435,578 زيادة — أكتر من 54% من الشغل اختفى في الهوا. وكل مرة تشغّل الكود هيطلعلك رقم مختلف، لأن السباق نفسه عشوائي.
سيناريو واقعي: لو عندك API بيخصم من مخزون منتج (stock) وجاله 500 طلب شراء في نفس الثانية، نفس السباق ده ممكن يخلّيك تبيع كمية أكبر من اللي عندك فعلاً. دي مشكلة overselling حقيقية بتحصل في المتاجر تحت الضغط.
ليه بيحصل ده بالظبط
ركز في السطرين local = counter و counter = local + 1. بين السطرين دول فيه نافذة زمنية صغيرة. لو خيط تاني قرا في النافذة دي، هياخد نفس القيمة القديمة، وكل الاتنين هيكتبوا نفس الرقم. النتيجة: زيادتين اتحسبوا كزيادة واحدة. ده بالظبط اللي حصل مع درج الكاشير.