لما حاجتين يشتغلوا في نفس الوقت
نظام التشغيل نفسه أول برنامج اضطر يتعامل مع حاجات كتير شغالة مع بعض: Interrupts، و Processes كتير، وكذا Core بيلمسوا نفس الداتا. وأي برنامج بيستخدم Threads بيقابل نفس المشكلة. الدرس ده بيوريك ليه سطر زي count++ ممكن يطلّع رقم غلط، وده كل اللي مرحلة الـ Concurrency هتحله.
- تعرّف الـ Thread وتفرّقه عن الـ Process
- تفهم ليه
count++مش عملية واحدة - تشوف Race Condition خطوة خطوة
- تعرف الأدوات اللي بتحلها: Atomic Operations و Locks
1. Process و Thread
لحد دلوقتي كل Process ليها Address Space لوحدها، ومش بتشوف ذاكرة غيرها. بس ساعات البرنامج نفسه عايز يعمل كذا حاجة في نفس الوقت على نفس الداتا: سيرفر بيخدم 1000 اتصال، أو برنامج بيعالج صورة على 8 Cores.
| Process | Thread | |
|---|---|---|
| الذاكرة | Address Space لوحدها | مشتركة مع باقي الـ Threads في نفس الـ Process |
| لو وقع | بيقع لوحده | ممكن يوقّع الـ Process كلها بكل الـ Threads |
| التواصل | محتاج قناة (Pipe، Socket، Shared Memory) | بيقرا ويكتب نفس المتغيرات مباشرة |
| أمثلة | كل php-fpm Worker Process لوحده | الـ Threads في Java و Go (Goroutines) وفي MySQL نفسه |
الذاكرة المشتركة سريعة ومريحة، وهي نفسها مصدر المشكلة.
2. count++ مش عملية واحدة
متغير count مشترك، وكل Thread بيزوده بواحد مع كل Request. في الكود شكله سطر واحد، بس الـ CPU بيشوفه 3 أوامر:
load reg, [count] ; هات القيمة من الذاكرة لـ Register
add reg, 1 ; زوّد واحد على الـ Register
store [count], reg ; رجّع القيمة للذاكرةوالـ Timer Interrupt (أو Thread تاني على Core تاني) ممكن يدخل بين أي أمرين منهم.
load reg1, [count]reg1 = 1001- 2
load reg2, [count]reg2 = 100 add reg1, 1 store [count], reg1كتب 1013- 4
add reg2, 1 store [count], reg2كتب 101
lock(m)خده1- 2
lock(m)⏳ مستني load / add / store unlock(m)كتب 1013- 4
load / add / store unlock(m)كتب 102
دوس ▶ شغّل، أو امشي خطوة خطوة بالأسهم.
أي داتا مشتركة بين حاجتين شغالين في نفس الوقت، وفيه واحد منهم على الأقل بيكتب، محتاجة حماية. سطر واحد في الكود ممكن يبقى كذا خطوة في الـ CPU.
3. ده يخصك إزاي كـ Backend Developer؟
- PHP (php-fpm): كل Request في Process لوحدها ومفيش ذاكرة مشتركة، فالمشكلة دي مش بتظهر في الذاكرة. بس بتظهر في أي حاجة مشتركة بين الـ Processes: الداتابيز، و Redis، و الملفات. عداد في Redis بـ
GETثمSET= نفس الـ Race بالظبط، والحلINCR(أمر ذري). - Node.js: Thread واحد للـ JavaScript، فمفيش أمرين بيتداخلوا في النص. بس أي
awaitفي النص بيفتح باب: كود قرا قيمة، وعملawaitلحاجة، وبعدين كتب. - Go و Java و Laravel Octane: Threads أو Goroutines بذاكرة مشتركة فعلاً. هنا محتاج
sync.MutexأوAtomicIntegerأو تتجنب الحالة المشتركة خالص.
مرحلة الـ Concurrency في الكورس بتدخل في الـ Locks من جوه، وإزاي بتتعمل بالـ Hardware، والـ Condition Variables، والـ Deadlocks.
4. أسئلة
فكّر في إجابتك الأول، وبعدين افتح السؤال.
01الفرق بين Process و Thread؟
الـ Process ليها Address Space لوحدها ومعزولة عن غيرها. الـ Threads جوه نفس الـ Process بيشتركوا في الذاكرة والملفات المفتوحة، وكل Thread ليه Stack و Registers خاصين بيه. الـ Threads أخف وأسرع في التواصل، بس محتاجين حماية للداتا المشتركة.
02ليه count++ ممكن يطلّع نتيجة غلط مع Threads؟
لأنه 3 أوامر في الـ CPU: load ثم add ثم store. لو Thread تاني قرا القيمة قبل ما الأول يكتب، الاتنين هيكتبوا نفس القيمة وزيادة هتضيع.
03يعني إيه Race Condition؟ وإزاي تحلها؟
لما النتيجة بتعتمد على ترتيب تنفيذ الـ Threads بالظبط. بتتحل إنك تخلّي الجزء الحساس (Critical Section) يتنفذ من Thread واحد في الوقت، بـ Lock (Mutex)، أو تستخدم Atomic Operation زي INCR في Redis أو AtomicInteger.
04PHP مفيهوش Threads. يبقى مفيش Race Conditions؟
لأ. كل Request في Process لوحدها، بس كلهم بيشتركوا في الداتابيز و Redis والملفات. قراية قيمة وبعدين كتابتها (Read-Modify-Write) على أي مورد مشترك فيها نفس الـ Race، وحلها عمليات ذرية (UPDATE x = x + 1، INCR) أو Locks.