المقدمة
الـ Concurrent Programming مبنية على فكرة إن يكون عندي الـ Application متكسر لـ Independent Tasks يعني مهام مستقلة أو Units اقدر اشتعل عليهم بشكل Concurrently.
وطبعًا الـ Abstraction Layer واللي بتسهلنا أغلب الشغل ده ألا وهو الـ OS قادر إنه يدير الـ Tasks دي ويعملهم Handling بشكل كويس من خلال إنه يوفر الـ Resources اللازمة بشكل كفء.
فنقدر نعتبر الـ Concurrent Programming هي مجموعة من الـ Abstractions اللي بتسمح للمطورين إنهم ينظموا التطبيق بتاعهم على شكل مهام صغيرة ومستقلة وينقلوها بعد كده للـ Runtime System واللي في الحالة دي هو الـ OS عشان يعملهم Handling بكفاءة.
وطبعا الـ Runtime System هيكون المسئول عن تنفيذهم وعملية الـ Orchestration اللي هتتم بينهم عشان يختار انهي Task اللي هيتم تنفيذها ويستغل الـ Resources بأفضل شكل ممكن.
وخلاصة الـ Abstractions اللي عمالين نتكلم عليها دي هي إن احنا عندنا بكل بساطة نوعين من الـ Abstractions كمطورين عشان نتعامل مع الـ Concurrent Programming والنوعين دول هم الـ Building Blocks اللي ممكن نعتمد عليهم عامة في تطبيق الـ Concurrency ألا وهم الـ Process والـ Threads.
كنا اتكلمنا في ورقة وقلم عن الفرق بينهم قبل كده تقدروا تشوفوه من هنا:
الـ Process
التعريف اللي أغلبنا سمع عنه للـ Process هي إنها عبارة عن Running Program ، فأي تطبيق أو برنامج شغال ده بنسميه Process واللي أغلبنا يقدر يشوفه من خلال الـ Management Tools على أي OS طبعًا ويشوف الـ Processes اللي موجودة.
فأي برنامج في الأساس هو عبارة عن حاجة ملهاش قيمة ، مرمية على الـ Disk ، بتتكون من مجموعة أوامر مستنية إنها تتنفذ ، والـ OS الله يمسيه بالخير بياخد الأوامر أو الـ Instructions دي وينفذهم على الـ Hardware بتاعنا فيبدأ البرنامج يبقى ليه قيمة فعلية لما يشتغل ويتنفذ.
مثال من الواقع على الـ Process
طبعًا أمثلة ده كتير في الواقع بتاعنا ، وعشان نبسط الكلام بمثال: فالعربية هي عبارة عن مجموعة من الأجزاء الميكانيكية اللي طبعا ملهاش أي قيمة وهي مبتتحركش ، ولكن بمجرد ما بندور العربية ونشغلها والمحرك بتاعها يبدأ يشتغل والعربية تتحرك بتتحول لحاجة قيمة ومفيدة بتساعدك في حياتك انك تتنقل بيها.
فنقدر نعتبر الـ Source Code بتاعنا كـ Developers هو العربية دي بس بدل مهو عبارة عن Mechanical Parts أو أجزاء ميكانيكية ، فهو عبارة عن مجموعة من الأوامر اللي مستنية يبقى ليها قيمة فعلية ، وبيشتغل على Abstractions وده اللي بتيحه لغات البرمجة.
فأنت فعليا كـ Developer معندكش Memory تخزن فيها بيانات أو ملفات تقرأ وتكتب منها وعليها أو بعض الأجهزة اللي بتبعتلها Signals فعلية عشان تقوم بدور معين ، ولكن احنا كـ Developers بنستعمل Models مبنية على Abstractions بفضل لغات البرمجة اللي شغالين بيها والـ Runtime Environment اللي بتنفذ الكلام ده على الـ Hardware الفعلي بتاعنا.
فالـ Abstraction اللي بيوفره الـ OS للـ Running Program بتاعنا هو الـ Process وطبعا مفيش حاجة اسمها Process على Scope الـ Machine Instructions فهو مجرد Abstraction ليه دور مهم في عزل الـ Tasks أو المهام اللي مفروض تتنفذ ويكون ليها Hardware Resources عشان تتنفذ.
فنقدر نلخص الكلام ده بأن الـ Processes دي محتاجة تتشارك مع الـ Hardware ويكون ليها Resources عشان تتنفذ وبيجي هنا دور الـ OS إنه يضمن ويحقق العلاقة بين الـ Process والـ Resources اللي هي محتاجاها من الـ Hardware وبالتالي عشان ده يحصل كل Process محتاج يكون ليها بعض البيانات المستقلة زي الـ Address Space والـ File Table ، ومن ثم الـ Process دلوقتي أصبحت عبارة عن وحدة مستقلة محتاجة يتوفرلها بعض الـ Resources.
الـ OS مش بس بيكون مسئول عن الـ Resource Allocation للـ Processes ولكن ده كمان بيوهم كل Process إنه ليها الحرية المطلقة في التعامل مع الـ Computer من خلال الـ Resources اللي هيوفرها ، حتى لو كان فيه كذا process شغالة في نفس الوقت.
وطبعًا عشان يحافظ على الوهم ده، الـ OS بيهتم إنه يتحكم في الـ processes ويحميهم ويعزلهم عن بعض. ده بيشمل التحكم في تخصيص الـ CPU cores والـ memory لكل process.
فالميزة الأساسية للـ processes هي الاستقلالية الكاملة والعزل أو الـ Isolation في التنفيذ عن باقي النظام، وده بيمنع أي تداخل عشوائي مع البيانات، وبيضمن إن أي crash يحصل في برنامج مايأثرش على باقي البرامج وده لإنهم معزولين عن بعض.
بس الميزة دي ليها عيب ، الـ processes مستقلة عن بعضها وده بيخلي التواصل بينها صعب جدًا وبشكل رسمي، الـ processes مفيش حاجة مشتركة بينها تقريبا، وأي تواصل حقيقي بين الـ processes محتاج استخدام آليات تانية، بتكون أبطأ بكتير من الوصول المباشر للبيانات.
التفاصيل الداخلية للـ Process
زي ما قولنا، الـ process هو مجرد برنامج شغال. في أي وقت، ممكن نجمع الـ process عن طريق إننا نعمل قايمة بكل الأجزاء المختلفة في نظام الكمبيوتر اللي بتوصلها أو بتعدلها وقت التشغيل:
- البيانات اللي الـ process بيقراها أو بيكتبها متخزنة في الـ memory. عشان كده، الذاكرة اللي الـ process بيشوفها أو بيوصلها (الـ address space) هي جزء من الـ process اللي شغال.
- الـ executable file اللي بيتم تنيفذه بكل الـ machine instructions هو جزء من الـ process.
- الـ process كمان محتاج حاجة تعرفه: اسم مميز نقدر نعرف بيه الـ process. وده بنسميه الـ process ID (PID).
- أخيرا، البرامج غالبا بتوصل للـ disks، أو network resources، أو أجهزة تانية. المعلومات دي لازم تتضمن قايمة بالملفات المفتوحة حاليا بالـ process، واتصالات الشبكة المفتوحة، وأي معلومات تانية عن الـ resources اللي بيستخدمها.
فباختصار الـ process بيحتوي على حاجات كتير: الـ executable file، مجموعة الـ resources اللي بيستخدمها (ملفات، اتصالات، إلخ)، والـ address space بالمتغيرات الداخلية. كل ده بنسميه الـ execution context. عشان في حاجات كتير جوة الـ processes.
وعشان كده عملية إنشاء process جديد يعتبر حاجة تقيلة. عشان كده بنسميها أحيانا heavyweight processes.
حالات الـ Process
لو بصينا على الـ process من منظور عالي شوية، هنلاقي إن كل حاجة بتبان بسيطة. في الأول، الـ process مش موجود. بعد كده بيتم إنشاؤه وتهيئته، وبعدها بيبقى موجود في مكان ما في ذاكرة الكمبيوتر (حالة الـ Created).
لما الكود بتاع المستخدم يبدأ process، الـ process بيروح لحالة الـ Ready—بيبقى جاهز إنه يتنفذ على الـ CPU في أي لحظة، بس لسه ما بيعملش حاجة. هو محتاج CPU عشان يبدأ التنفيذ.
بعد كده الـ OS بيختار الـ process اللي هيتنفذ على الـ CPU من قايمة الـ processes الجاهزة للتنفيذ. بعد ما الـ OS يختار process، الـ process المختار بيروح لحالة الـ Running.
الـ processes غالبا بيتم إنشاؤها عن طريق الـ OS. بالإضافة لإنشاء الـ processes، الـ OS كمان مسئول عن إنهاء الـ processes. ودي مش مهمة بسيطة. الـ OS محتاج يفهم إن الـ process خلص—يا إما المهمة خلصت، أو الـ process فشل ولازم ننضفه، أو الـ parent process مات.
فعملية إنشاء أو إنهاء الـ process هي عملية مكلفة نسبيا، عشان زي ما شوفنا، الـ process ليها موارد كتير مرتبطة بيها، ولازم يتم إنشاؤها أو تحريرها. وده بالطبع بياخد وقت من النظام وبيزود وقت الاستجابة.
الـ Multiple Processes
أي process ممكن تنشئ الـ processes الخاصة بيها —بنسميها child processes— عن طريق system calls زي fork()
أو spawn()
وبالتالي بنقول احيانا على العملية دي Forking أو Spawning.
الـ child processes هي نسخ مستقلة من الـ process الأساسية، وليها مساحة ذاكرة مستقلة بذاتها، وده معناه إن الـ process بتشتغل بشكل مستقل ومعزولة عن الباقي عن طريق الـ OS ، وبرضو مش ممكن توصل للبيانات بتاعة أي processes تانية بشكل مباشر، وكذلك تعليمات كل process بتتنفذ في الـ process الخاصة بيها بشكل مستقل، فممكن نعتبر إن كل process شغالة بشكل Parallel مع التانيين.
بما إننا دلوقتي داخلين في منطقة الـ concurrency من خلال استخدام الـ spawning، وعندنا أكتر من process شغالة فممكن بالتالي نقسم عملية التنفيذ البرنامج بتاعنا لـ processes متعددة ممكن تتنفذ في نفس الوقت على parallel hardware.
عملية الـ Spawning في لغات البرمجة
لغات البرمجة غالبا بيكون فيها تجريدات (Abstractions) عالية المستوى أو طرق للتعامل مع الـ processes، عشان بتكون أسهل في الصيانة والتتبع في الكود بتاعنا.
طريقة الـ forking/spawning في كذا technologies مشهورة بتعتمد على الـ preforking وده معناه إن السيرفر بيعمل forks وقت تشغيل السيرفر، وبعدين الـ forks دي بتتعامل مع الطلبات اللي جاية.
فمثلًا NGINX و Apache HTTP Server بيشتغلوا بالطريقة دي، وده بيخليهم يقدروا يتعاملوا مع مئات الـ requests. ولكن الحلول دي كمان بتدعم طرق تانية كذلك.
في الختام
بكده نكون شوفنا مقدمة عن الـ Building Blocks للـ Concurrency وشوفنا مفهوم الـ Concurrent Programming والـ Independent Tasks وشوفنا أهم مكونين رئيسين وهم الـ Process والـ Thread ، اتكلمنا بشكل مختصر عن الـ Process وازاي الـ OS بيجردلنا الدنيا ويبسط علينا العلاقة بين الـ Process والـ Resources اللي محتاجين يحصلها Allocation للـ Process.
اتكلمنا برضو شوية عن الـ Process Internals بتتكون من ايه والحالات بتاعتها وليه عملية إنشاء وإنهاء Process بتكون عملية مكلفة وبالتالي بتكون Heavyweight Process وعرفنا ان بالشكل ده عشان الاستقلالية والعزل اللي بتتميز بيه الـ Process صعب يتم مشاركة البيانات والتواصل بين الـ Process وبعضها ، وان الموضوع بيكون بطيئ شوية.
Discussion