







السمعة:
كيف الحال جميعًا، عساكم بألف خير يارب؟
أعتذر عن الإنقطاع في كتابة المواضيع التقنية، لنا رجعة قويّة بإذن المولى تعالى.
الحوسبة المتوازية أو ما يُعرف بال Parallel Computing.
بدايةً: دعنا نأخذ مثال بسيط كي نفهم ماهي
تخيّل أنك موجود في محل صيانة للسيارات وفي داخل المحل يوجد موظف واحد يقوم بكل أعمال الصيانة، ف من الطبيعي أن يكون عمل هذا الموظف بطيء عندما يكون عنده أكثر من سيارة أو حتى أكثر من عطل في السيارة!
ولو نظرت للموضوع كمشكلة وتريد حل لها، ف البديهيات تقول لك: هل يُمكن زيادة عدد الموظفين؟
هذا السؤال جوهري جدًّا وله أهمية عظيمة بالموضوع اللي نتكلم فيه اليوم، ودعني آخذك الى البدايات يا رفيقي،
حيث أنه بعام 1945 تم إصدار اول نموذج للهيكلة المعمارية للحاسوب تحت اسم Von Neumann Architecture
وكانت الفكرة على أن الحاسوب يملك وحدة معالجة مركزية "CPU" يُخزن البيانات والتعليمات في نفس الذاكرة Stored-program computer وكان يعتمد على معالج أحادي النواة وهذا ما يعني أن التسلسل المنطقي يكون على شكل قراءة التعليمة ثم تنفيذها ثم الانتهاء منها، وهذا ما يعني بأن علينا الانتظار كثيراً في بعض الأحيان حتى ينتهي المعالج ثم الانتقال الى العمليات التالية وينفذها جميعاً بشكل "تسلسلي"
وكان أول كمبيوتر في العالم قابل للبرمجة اسمه ENIAC
ENIAC |
وبعد ذلك ومع تطور الحاجة للعمليات الحسابية وصل المهندسين الى نقطة مغلقة في حياة هذا النوع من الكمبيوتر وابتكروا حلّاً ليس بالحل الجوهري وانما هو تخطي لمشكلة بفترة مؤقتة وكان اسمه:
Pseudo Multi-Tasking أو ما يُعرف بـ Time-Sharing Systems
فكرة هذا الحل ببساطة هو أن نقوم بتقسيم المدة الزمنية المراد منها الانتهاء من 4 عمليات الى شرائح زمنية قصيرة جداً بالملي ثانية أحياناً، والهدف منها هو عندما تريد النواة أن تقوم بمعالجة أول عملية فإنها بعد فترة زمنية تقطع معالجة العملية الاولى وتنتقل الى العملية الثانية وبينما تبقى العملية الاولى تنتظر وعندما تقوم النواة بمعالجة جزء معين من تعليمات العملية الثانية فإنها تنتقل الى الثالثة وتنقسم هذه العمليات الى قسمين رئيسيات:
القسم الأول: شرائح زمنية Time Slicing
وهنا يقوم نظام التشغيل بتقسيم وقت المعالج الى شرائح زمنية قصيرة قد تكون بالملي ثانية أو اقل، ويتم تخصيص هذا الجزء لكل عملية بحيث أنها تستطيع انجاز عدد محدد من العمليات قبل أن ينتقل المعالج الى العملية التي تليها.
القسم الثاني: تبديل العمليات Context switching
عند الانتهاء من الشريحة الزمنية المخصصة للعملية الفُلانية، فإن المعالج يقوم بتبديل العمليات والإنتقال الى العملية التالية تحت اسم Ready State ويستخدم نظام التشغيل ال Interrupts او SysCall
وتخصيص وقت زمني محدد لكل عملية حتى وإن لم يتم الانتهاء منها، كانت تُشعر المستخدم بأن النظام اسرع

المهم كانت هذه التقنية السبب الرئيسي وراء الخروج بنظام التشغيل UNIX
وفي الثمانينات والتسعينات ظهر ما يٌسمى بال MultiThreading CPU's وهنا يُمكننا أن نقول بأن الإنفراجة الأولى للكمبيوتر بدأت حيث أن هذا التطور جاء ليُعالج المشاكل الحتمية والسابقة من عمليات المعالجة
لا سيما بأن الكمبيوتر وبرامج الكمبيوتر استمرت في التطور ويُمكننا توضيح ال Threads بأنها مجموعة من الخيوط تنبثق من النواة Core والتي بدورها تقوم بالتشارك في الموارد المخصصة للعملية الواحدة من الوصول الى الذاكرة والملفات المفتوحة
وتُبيّن الصورة في الأعلى بأن الخيوط قد تشترك في الملفات والترميزات والبيانات ولكن لكل خيط منها stack و registers خاصة بها
دعنا نطرح مثالاً:
يمرّ معنا كل يوم حتى نفهم أهمية الخيوط في حياتنا، لو فتحنا برنامج word للكتابة النصية ف وقمنا بكتابة "السلام عليكم"، الآن يقوم خيط بفحص صحّة الكتابة اللغوية ويقوم خيط آخر بإنتظار المستخدم لإدخال الجديد من النص وإذا أردت تغيير اللون او الخط فهذا يتم تنسيبه إلى خيط ثالث وهكذا. . . .
وهنا نفهم بأن كل حركة داخل ال Word هي تُسمى thread بينما ال word بحد ذاته يٌسمى Process .
ومع وصول المعالجات الى حدودها الفيزيائية ببداية الألفية الثانية، انتهى الغداء المجّاني، وما أقصده هنا بالغداء المجاني الا وهو أن المبرمجين لم يكونوا يعتمدوا على تطوير برامجهم لتحسين سرعتها والاداء إنما كان الاعتماد بشكل كامل على الشركات التي تصنع المعالجات لأن البرامج كانت تعتمد على ال single process معالج احادي النواة، والآن بدأ العصر الذهبي للمعالجات متعددة الأنوية MultiCore Process وزيادة سرعة التردد ينتج عنها زيادة في استهلاك الطاقة وزيادة في الانبعاث الحراري.
وفي عام 2006 أي قبل 19 عام من الآن ظهر أول معالج تجاري يمتلك أكثر من نواة Core 2 duo
وهذا سمح بحدوث التوازي الحقيقي حيث تعمل البرامج متعددة الخيوط على أنوية فعلية حقيقية بدلاً من تبديل المهام بينها.
واستمر التطور في تصنيع هذه المعالجات الى يومنا هذا حتى وصلنا الى معالج xeon max بقدرة 144 نواة ! وتكلفة لا تقل عن 8000 دولار!
وبعد هذه المقدمة التاريخية والنظرية ..
دعنا نخوض قليلاً في طريقة كتابة البرمجة الموازية :
كي تستطيع استخدام ال Thread عليك باستدعاء المكتبة الخاصة بها وهي بلغة ال C++ تكون :
C++:
#include <thread>
وبعدها دعنا نكتب أول برنامج لنا بإستخدام الThreads ونطبع Hello World!
C++:
#include <iostream>
#include <thread>
using namespace std;
void hello()
{
cout << "Hello Concurrent World\n";
}
int main()
{
thread t(hello);
t.join();
}
الجديد هنا هو أننا قمنا بطباعة Hello Concurrent World داخل دالة hello وقمنا بإستدعاء الدالة عن طريق thread ولكي نضمن عدم انتهاء العملية قبل انتهاء الخيط قمنا بإستخدام t.join
حسنًا، الآن لننتقل الى مقارنة سريعة جداً ما بين البرمجة التسلسلية و البرمجة المتوازية
لقد قمت بإستخدام برنامج مكتوب بلغة ال ++C يقوم على جمع الأرقام من 1 الى 10 مليار ثم نقوم بحساب الزمن المستغرق لعملية الجميع لكل من البرمجة التسلسلية والبرمجة الموازية وبعد ذلك نقوم بإيجاد الفارق الزمني بينهم
وقمنا بإستخدام مكتبة Chrono لحساب الزمن واستخدمنا 4 خيوط فقط.
C++:
#include <iostream>
#include <thread>
#include <vector>
#include <chrono>
using namespace std;
using namespace std::chrono;
void sumRange(unsigned long long start, unsigned long long end, unsigned long long& result) {
unsigned long long sum = 0;
for (unsigned long long i = start; i <= end; ++i) {
sum += i;
}
result = sum;
}
int main() {
const unsigned long long N = 10000000000;
unsigned long long serialSum = 0;
auto t1 = high_resolution_clock::now();
for (unsigned long long i = 1; i <= N; ++i) {
serialSum += i;
}
auto t2 = high_resolution_clock::now();
double serialTime = duration<double>(t2 - t1).count();
cout << "Sequential: " << serialSum << endl;
cout << "Time of Sequential: " << serialTime << " sec" << endl << endl;
const int numThreads = 4;
vector<thread> threads;
vector<unsigned long long> results(numThreads, 0);
unsigned long long blockSize = N / numThreads;
t1 = high_resolution_clock::now();
for (int i = 0; i < numThreads; ++i) {
unsigned long long start = i * blockSize + 1;
unsigned long long end = (i == numThreads - 1) ? N : (i + 1) * blockSize;
threads.push_back(thread(sumRange, start, end, ref(results[i])));
}
for (auto& th : threads) {
th.join();
}
unsigned long long parallelSum = 0;
for (int i = 0; i < numThreads; ++i) {
parallelSum += results[i];
}
t2 = high_resolution_clock::now();
double parallelTime = duration<double>(t2 - t1).count();
cout << "MultiThreads: " << parallelSum << endl;
cout << "Time Of MultiThreads: " << parallelTime << " sec" << endl << endl;
cout << "Time Diff "
<< (serialTime - parallelTime) << " sec" << endl;
return 0;
}
كل ما ذُكر مسبقاً ما هو إلا قشور علمية ليس أكثر
وإن شاء الله سنرفع على المستودع كتب خاصة بال Parallel Programming
وفي مواضيع أخرى قد نطرح بعض الجوانب السلبية وكيفية حلها بإذن الله
دُمتم بخير
التعديل الأخير بواسطة المشرف: