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

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

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

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

الدعم

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

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

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

Race Condition للمبتدئ: ليه عدّادك بيضيّع نص الزيادات

مبتدئ٢٥ يونيو ٢٠٢٦5 دقائق قراءة
Race Condition للمبتدئ: ليه عدّادك بيضيّع نص الزيادات

المستوى المطلوب: مبتدئ. هنبدأ بمثال من الحياة، وبعدين نفكّك المفهوم علميًا. كل الكود في المقال قابل للنسخ والتشغيل على Python عادي.

لو خيطين (threads) في برنامجك بيزوّدوا نفس الرقم في نفس اللحظة، البرنامج ممكن يحسبلك زيادة واحدة بدل اتنين — من غير أي رسالة خطأ ومن غير ما الكود يقع. ده اسمه Race Condition، والمقال ده هيوريك بالظبط ليه بيحصل وإزاي تمنعه في سطر واحد.

Race Condition: لما خيطين يتسابقوا على نفس القيمة

المثال الأول: درج الكاشير

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

دلوقتي بصّ على اللي بيحصل فعلاً لما يتحركوا في نفس اللحظة: الكاشير الأول فتح الدرج ولقى 100. في نفس اللحظة الكاشير التاني فتح ولقى 100 برضه. الأول حسب 110 وكتبها. التاني — اللي لسه شايف 100 — حسب 110 وكتبها. الدرج بقى فيه 110، رغم إن المفروض يكون 120. عشرة جنيه اختفوا، ومفيش حد عمل غلط واضح.

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

الكاشير هنا = الخيط (thread). الدرج = متغيّر مشترك في الذاكرة. عملية "زوّد واحد" مش خطوة واحدة، هي تلات خطوات: اقرأ القيمة، عدّل عليها، اكتبها. الاسم العلمي للنمط ده هو read-modify-write. المشكلة إن الخطوات التلاتة دي مش ذرّية (atomic)، يعني خيط تاني يقدر يدخل بينها ويقرا قيمة قديمة قبل ما الأول يخلّص كتابة. لما ده يحصل، زيادة بتضيع.

نوريك المشكلة بكود حقيقي

الكود ده بيشغّل 4 خيوط، كل واحد بيزوّد عدّاد مشترك 200 ألف مرة. المفروض الناتج يكون 800 ألف بالظبط:

Python
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. بين السطرين دول فيه نافذة زمنية صغيرة. لو خيط تاني قرا counter في النافذة دي، هياخد نفس القيمة القديمة، وكل الاتنين هيكتبوا نفس الرقم. النتيجة: زيادتين اتحسبوا كزيادة واحدة. ده بالظبط اللي حصل مع درج الكاشير.

نقطة بتخدع المبتدئين: لو كتبت counter += 1 على طول، في Python غالبًا مش هتشوف المشكلة بسبب حاجة اسمها الـ GIL بتخلّي خيط واحد بس يشتغل في اللحظة. بس ده مش أمان حقيقي، ده مجرد إخفاء. أول ما النافذة تكبر (زي ما عملنا فوق)، أو تشتغل على Python بدون GIL (نسخة free-threaded في 3.13+)، أو لغة زي Go أو Java، الباج بيظهر صريح. الافتراض اللي بيوقّعك إن "الكود اشتغل صح عندي مرة" يعني آمن — وده غلط.

الحل: القفل (Lock)

القفل بيضمن إن خيط واحد بس يدخل المنطقة الحرجة في المرة. اللي عايز يدخل لازم يستنى لحد ما اللي جوه يخرج. كده القراءة والكتابة بيبقوا حركة واحدة ملزوقة، مفيش حد يقدر يقرا قيمة نص-محدّثة:

Python
import threading

counter = 0
lock = threading.Lock()

def work():
    global counter
    for _ in range(200000):
        with lock:            # خيط واحد بس جوه البلوك ده
            counter += 1

threads = [threading.Thread(target=work) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(counter)   # 800000 بالظبط، في كل تشغيل

دلوقتي الناتج بقى 800,000 مظبوط في كل مرة. القفل حوّل الـ read-modify-write لعملية ذرّية فعليًا.

الـ trade-off هنا

القفل مش ببلاش. هو بيخلّي الخيوط تستنى بعض، فبتخسر سرعة. في نفس تجربتنا، النسخة من غير قفل خلّصت مليونين عملية في حوالي 80 مللي ثانية، والنسخة بالقفل خدت حوالي 560 مللي ثانية — أبطأ حوالي 7 مرات. الخلاصة: بتكسب صحّة النتيجة، بتخسر throughput. الافتراض إن العملية اللي بتقفل عليها قصيرة جدًا. لو طوّلت اللي جوه القفل (زي ما تعمل I/O وانت قافل)، بتحوّل القفل لعنق زجاجة بيوقّف كل الخيوط ورا بعض.

متى لا تشغل بالك

مش كل كود متعدد الخيوط محتاج أقفال. تجاهل الموضوع لو:

  • كل خيط بيشتغل على بياناته الخاصة ومفيش حالة مشتركة بيتكتب فيها — مفيش سباق من الأساس.
  • القيمة المشتركة بتتقري بس ومحدش بيكتبها بعد ما الخيوط بدأت — القراءة لوحدها آمنة.
  • تقدر تستخدم بنية جاهزة thread-safe بدل ما تدير القفل بنفسك، زي queue.Queue في Python، أو الأنواع الذرّية في لغات تانية (مثل atomic.AddInt64 في Go) اللي بتكون أرخص من قفل كامل للعدّادات البسيطة.

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

روح دلوقتي لأي متغيّر مشترك بين خيوط في كودك بيتكتب — عدّاد، dict كاش، رصيد، حالة — واسأل سؤال واحد: هل ممكن خيطين يكتبوه في نفس اللحظة؟ لو الإجابة آه، لفّه بـ Lock أو استبدله ببنية thread-safe. وكبداية، انسخ الكود الأول فوق، غيّر عدد الخيوط من 4 لـ 8، وشوف الرقم بيقل أكتر إزاي. لما تشوف الضياع بعينك، هتفتكر تقفل في المرة الجاية.

المصادر

  • توثيق Python الرسمي — وحدة threading والـ Lock: docs.python.org/3/library/threading.html
  • توثيق Python — تعريف الـ Global Interpreter Lock (GIL): docs.python.org/3/glossary.html
  • PEP 703 — جعل الـ GIL اختياريًا في CPython (النسخة free-threaded): peps.python.org/pep-0703
  • توثيق Python — sys.setswitchinterval (تحكّم في تبديل الخيوط): docs.python.org/3/library/sys.html

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

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

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