الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

الـ Race Condition: ليه تطبيقك بيخصم المبلغ مرتين تحت الضغط

متوسط19 يوليو 20265 دقائق قراءة
الـ Race Condition: ليه تطبيقك بيخصم المبلغ مرتين تحت الضغط

المستوى: متوسط — مناسب لمطور بيكتب كود بيتنفّذ بالتوازي (threads، عمّال، أو أكتر من نسخة من التطبيق) وحاسس إن فيه أرقام بتضيع من غير سبب واضح.

الـ Race Condition: لما اتنين بيلمسوا نفس الرقم في نفس اللحظة

لو رصيد العميل بينقص غلط لمّا طلبين يوصلوا في نفس المللي ثانية، ركّز: المشكلة مش في الداتابيز ولا في السيرفر. ده اسمه Race Condition، وهتعرف هنا إزاي تكتشفه بكود بيقيسه قدامك، وتصلّحه في سطرين.

المشكلة باختصار

تخيّل حساب بنكي فيه 100 جنيه، ومعاه بطاقتين. الزوجة على ماكينة ATM في مصر، والزوج على ماكينة في دبي، والاتنين ضغطوا "اسحب 10" في نفس اللحظة بالظبط.

الماكينة الأولى قرأت الرصيد: 100. الماكينة التانية قرأت الرصيد كمان: 100 — قبل ما الأولى تخلّص. الأولى حسبت 100 - 10 = 90 وكتبت 90. التانية حسبت نفس الحاجة، 100 - 10 = 90، وكتبت 90. طلعت فلوس من الجيبين، بس الرصيد نقص 10 بس بدل 20. البنك خسر عشرة جنيه. ده بالظبط اللي بيحصل في السيرفر بتاعك.

خيطان Thread A و Thread B يقرآن نفس قيمة الرصيد 100 في نفس اللحظة قبل الخصم

التعريف العلمي: اللي بيحصل فعلاً

الـ Race Condition هي إن ناتج البرنامج يعتمد على ترتيب أو توقيت تنفيذ عمليات بتشتغل بالتوازي — وده ترتيب انت مش متحكّم فيه.

الجذر هنا عملية اسمها read-modify-write. سطر بسيط زي balance -= 10 مش خطوة واحدة، ده تلات خطوات: اقرأ القيمة الحالية، اطرح منها، ارجع اكتب الناتج. لو خيط تاني قرأ القيمة القديمة بين خطوة القراءة وخطوة الكتابة، تحديثه هيمسح تحديثك. الظاهرة دي اسمها lost update (التحديث الضائع).

خلّينا نقيسها بالكود

الكود ده بيشغّل خيطين، كل واحد بيزوّد عدّاد مشترك مليون مرة. المفروض الناتج 2,000,000. جرّبه بنفسك.

Python
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) — يعني متتجزّأش. القفل بيضمن إن خيط واحد بس يدخل الجزء الحسّاس في نفس اللحظة، والباقي يستنّى دوره.

Python
import threading

counter = 0
lock = threading.Lock()

def add_a_million():
    global counter
    for _ in range(1_000_000):
        with lock:          # خيط واحد بس جوّه في اللحظة دي
            counter += 1

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 كل مرة

دلوقتي الناتج 2,000,000 بالظبط في كل تشغيلة. الجزء المحمي بين with lock اسمه القسم الحرِج (critical section).

الـ trade-off هنا

القفل بيكسّبك صحّة البيانات، بس بيكلّفك سرعة. في التجربة اللي فوق، النسخة بالقفل خدت حوالي 0.9 ثانية، والنسخة من غير قفل خدت حوالي 0.12 ثانية — يعني أبطأ حوالي 7 مرات. السبب إن كل خيط بيقف يستنّى القفل، والتزامن الحقيقي بيقل.

القاعدة: خلّي القسم الحرِج أصغر ما يمكن. اقفل على السطر اللي بيعدّل المشترك بس، مش على حسابات تقيلة أو نداءات شبكة. الافتراض إن التعديل نفسه سريع؛ لو جوّه القفل فيه I/O، القفل هيتحوّل لعنق زجاجة يشلّ التطبيق كله.

مش بس في الـ Threads

لو تطبيقك ويب بيشتغل على أكتر من عملية أو أكتر من سيرفر، الـ Lock بتاع الذاكرة مش هيكفي — كل عملية ليها القفل بتاعها. هنا بتنقل الحماية لطبقة مشتركة:

  • الداتابيز: استخدم SELECT ... FOR UPDATE جوّه transaction، أو تحديث ذرّي زي UPDATE accounts SET balance = balance - 10 WHERE id = 1 AND balance >= 10. السطر ده بيخلّي القراءة والكتابة خطوة واحدة عند الداتابيز.
  • قفل موزّع: زي Redis بنمط SET key value NX PX 5000 لمّا تحتاج تنسّق بين أكتر من خدمة.

متى لا تستخدم القفل

مش كل مشكلة محتاجة قفل. لو الكود بتاعك single-threaded أصلاً (زي أغلب سكربتات Python البسيطة، أو كود Node على event loop واحد)، مفيش خيطين بيتصارعوا، فالقفل تعقيد بلا فايدة. وكمان لو كل خيط بيشتغل على بياناته لوحده ومفيش حالة مشتركة، سيبها من غير قفل. القفل تكلفته حقيقية — استخدمه بس لما يبقى فيه حالة مشتركة قابلة للتعديل من أكتر من مكان في نفس الوقت.

ملحوظة مهمة عن Python: بسبب الـ GIL ممكن حاجات معيّنة تبان آمنة بالصدفة، بس ده سلوك مش مضمون ويتغيّر بين الإصدارات — متعتمدش عليه أبدًا كبديل عن القفل.

الخطوة التالية

افتح أي مكان في كودك بيعمل x += 1 أو "اقرأ رصيد ثم اكتبه" على حالة بيوصلها أكتر من خيط أو أكتر من طلب. سطر واحد كده كفاية يبوّظ الأرقام تحت الضغط. لو لقيت واحد، حوّله لعملية ذرّية: with lock في الذاكرة، أو UPDATE ... WHERE في الداتابيز. بعدها شغّل تجربة الـ counter اللي فوق على الكود بتاعك عشان تتأكد إن الرقم ثابت.

المصادر

  • توثيق Python الرسمي — threading.Lock وشرح الـ GIL.
  • PostgreSQL Documentation — Row-Level Locks و SELECT FOR UPDATE.
  • Redis Documentation — Distributed Locks (نمط SET NX).

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة