
السلام عليكــم ورحمـة الله وبركاتــه ،،
اخواني الاعزاء , لطالما استفدت من جهود الاخوة في منتديات الفريق العربي , فلا اقل من ان اشارك في اثراء هذه الجهود الطيبة وإن كان علمي القليل لا يرقى الى مستوى الاعضاء المخضرمين , الا انه يطيب لي ان اشارككم ولو بقدر بسيط مما تعلمته .
اغلب مبرمجي ++C وخاصة الهواة امثالي يقتصر فكرهم البرمجي فيما يعرف بالبرمجة التسلسلية او ما يسمى Serial Programming اي ان دوال البرنامج المصمم تنفذ دالة اثر اخرى بمعنى اخر لا يمكن في هذا النوع من البرمجة تنفيذ دالتين او اكثر في نفس الوقت .
لهذا ساتطرق في هذا الموضوع الى منهج برمجي اخر وهو ما يعرف بالبرمجة المتوازية او Parallel Programming والذي يمكننا من تنفيذ مهام عدة في نفس الوقت وهو ما يعرف بـ MultiTask , وسندرس احد التقنيات المستعملة لتنفيذ ذلك وهو مفهوم تعدد المسالك " الخيوط " او ما يعرف بـ
" MultiThread " .
لنعرف اولا ما هو الثريد او Thread : نستطيع وصف الثريد بانه اجراء يتم تنفيذه داخل عملية Process ويعمل بشكل منفصل عنها بحيث يخصص له وقت تنفيذ منفصل ويكون لهذا الـ Thread القدرة على مشاركة المصادر Resources " كالذاكرة مثلا " مع العملية الرئيسة Process .
بالطبع يمكننا انشاء اكثر من مسلك Thread تكون تابعة لنفس العملية Process وتتشارك نفس المصادر .
ويمكننا تخيل الثريد Thread كمسار مباشر يربط ما بين البرنامج و المعالج بحيث يتم تنفيذ التعليمات الموجودة في هذا المسلك بشكل مستقل بغض النظر عن تسلسل العمليات في البرنامج.
فائدة الثريد Thread تظهر بجلاء عند الحاجة لتنفيذ اكثر من وظيفة في نفس الوقت مثلا البحث في مصفوفة بيانات كبيرة بحيث تقوم دالتين بالبحث في نفس المصفوفة دالة تبحث من البداية و الدالة الاخرى تبحث من النهاية , بالطبع هذا سيوفر زمن اقل في الحصول على النتائج , ولنفهم طريقة عمل انظمة التشغيل التي تعرف بـ MultiTasks Operating Systems كالويندوز و اللينكس وهي انظمة قادرة على تنفيذ مهام متعددة في نفس الوقت " Simultaneously " وهذا اصطلاح نسبي ففي الحقيقة لا يستطيع المعالج تنفيذ اكثر من تعليمة واحدة في نفس الوقت والذي يحدث هو تقسيم وقت عمل المعالج على العمليات التي بالذاكرة بحيث ينفذ جزء من كل عملية ثم ينتقل لغيرها كل هذا يتم في وقت قصير جدا بحيث يبدو كأن جميع البرامج او العمليات تنفذ في نفس الوقت .
في الانظمة التي تعمل بمعالجات متعددة MultiProcessors وتعرف ايظا بـ Multi-Core يمكن ان تنفذ العمليات في نفس الوقت بحيث يتم توزيع العمليات على المعالجات الموجودة .
الاهمية الاخرى للمسلك او Thread كونه يستهلك جزء بسيط جدا من الذاكرة و وقت المعالجة مقارنة مع الـ Process
ميزة اخرى يجدر ذكرها وهي امكانية مشاركة المصادر Resources مع باقي المسالك الاخرى .
كيف يمكن الحصول على مسلك او Thread جديد :
توجد بعض المكتبات التي توفر هذه العمليات كمكتبة pThread في نظام التشغيل Linux " لا اعلم ان كانت توجد منها نسخة للويندوز " وبغض النظر عن هذه المكتبات يوفر نظام التشغيل Windows دوال API تمكننا من عمل ذلك بكل سهولة .
سنعمل اولا على أنشاء المسلك او الـ Thread باستخدام الدالة CreateThread والشكل العام لهذه الدالة كالاتي :
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes DWORD dwStackSize, // initial thread stack size LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags LPDWORD lpThreadId // pointer to receive thread ID );
عند تنفيذ الدالة بنجاح ترجع بمقبض Handle للثريد اما غير ذلك فترجع بالقيمة NULL .
تأخذ هذه الدالة ستة وسائط " معاملات " وهي كما يلي :
المعامل الاول : عبارة عن مؤشر لتركيبة خصائص الامان , ويحدد امكانية وراثة مقبض Handle الثريد الجديد من العملية الابن Child Process واذا تم اعطائه القيمة NULL فلن يكون بالامكان وراثة مقبض الـ Thread.
المعامل الثاني : يحدد حجم الستاك Stack Size للثريد الجديد وهو بحجم نوع البيانات DWORD , واذا تم اعطائه القيمة صفر 0 او اصغر سيتم اعطاء الثريد حجما افتراضيا يعادل حجم العملية التي استدعت الدالة CreateThread .
المعامل الثالث : وهو مؤشر من نوع LPTHREAD_START_ROUTINE لدالة معرفة مسبقا والتي سيتم منها بدء تنفيذ الـ Thread غالبا ما تسمى بـ ThreadProc , مع ملاحظة امكانية تسميتها باي اسم اخر وتكون عادة من النوع DWORD WINAPI وتأخذ باراميتر " معامل " من نوع مؤشر فارغ
(LPVOID) او (*void).
المعامل الرابع : ويحمل قيمة 32 بت يتم تمريرها لـ Thread الجديد .
المعامل الخامس : ويحمل Flag او " خاصية او علم " يحدد عمل الثريد , فعند اعطائه القيمة CREATE_SUSPENDED تقوم الدالة بانشاء الـ Thread و توقف عمله لحين ايقاضه او استئنافه باستخدام الدالة ResumeThread وعند اعطائه القيمة صفر 0 تعمل الدالة على انشاء الـ Thread ويبدأ تنفيذه فورا .
المعامل السادس : مؤشر لمتغير بحجم 32-bit يستقبل معرف الثريد Thread .
لنأخذ مثال بسيط يبين لنا الشرح السابق .
فسنقوم بتصميم برنامج بسيط تقوم الدالة الرئيسة main فيه بطباعة جملة " Main Function Called " ثم ستنشيء ثريد Thread وستنتظر 3 ثواني ثم تنهي البرنامج , والثريد Thread الذي تم انشائه سيقوم باستدعاء دالة تقوم بطباعة الارقام من 0 الى 9 بمعدل رقم كل ثانية , عليه سيكون البرنامج كالاتي:
#include <iostream>
using namespace std;
#include <windows.h>
DWORD WINAPI myThreadFunc(void*)
{
cout << "Thread Created !" << endl;
for ( int j = 0; j < 10; j++){
cout << "Thread Function Prints : " << j << endl;
Sleep(1000);
}
cout << "myThread's Function Ended" << endl;
return 0;
}
int main()
{
DWORD threadID;
cout << "Main Function Called\n";
CreateThread(NULL,0,myThreadFunc,0,0,&threadID);
Sleep(3000);
cout << "Program Should End Now !\n";
return 0;
}كما نلاحظ من الكود السابق ان الدالة main ستنشيء الثريد Thread ثم ستنتظر 3 ثواني ثم ستنهي البرنامج , و يتضح لنا ان الدالة التي يبدأ الـ Thread منها وهي الدالة myThreadFunc تحتاج الى 10 ثواني لاتمام طباعة جميع الارقام وانهاء الدالة .
و يظهر جليا ان البرنامج سينتهي قبل انتهاء الدالة myThreadFunc من طباعة جميع الارقام .
لحل هذه الاشكالية يوفر نظام التشغيل Windows الدالة WaitForSingleObject وتعمل على انتظار الكائن لحين انتهائه او رجوعه , والشكل العام لها كالاتي :
DWORD WaitForSingleObject( HANDLE hHandle, // handle to object to wait for DWORD dwMilliseconds // time-out interval in milliseconds );
هذه الدالة تاخذ وسيطين " معاملين " هما كالاتي :
المعامل الاول : مقبض Handle للكائن وسنستخدم له المقبض الذي تعود به الدالة CreateThread .
المعامل الثاني : وياخذ قيمة من النوع DWORD تمثل فترة الانتظار بالملي ثانية , ويمكن جعل فترة الانتظار لانهائية الى لحين رجوع او انتهاء الكائن وذلك باعطائه القيمة INFINITE , وعند اعطائه القيمة صفر 0 تقوم الدالة باختبار حالة الكائن ثم الرجوع فورا دون انتظار رجوع او انتهاء الكائن .
والقيم التي ترجع بها الدالة WaitForSingleObject هي :
القيمة WAIT_ABANDONED وذلك عند وجود mutex للكائن .
القيمة WAIT_OBJECT_0 تم انتهاء الكائن .
القيمة WAIT_TIMEOUT انتهاء فترة الانتظار بدون انتهاء الكائن .
في حالة فشل الدالة ترجع بالقيمة WAIT_FAILED .
حسنا لنطبق الان المفاهيم التي تعلمناها بحيث تنتظر الدالة الرئيسة main انتهاء دالة الثريد قبل انهاء البرنامج , وسيكون برنامجنا كالتالي :
#include <iostream>
using namespace std;
#include <windows.h>
DWORD WINAPI myThreadFunc( LPVOID )
{
cout << "Thread Created !" << endl;
for ( int j = 0; j < 10; j++){
Sleep(1000);
cout << "Thread Function Prints : " << j << endl;
}
cout << "myThread's Function Ended" << endl;
return 0;
}
int main()
{
DWORD threadID;
HANDLE ThreadhHandle;
cout << "Main Function Called\n";
ThreadhHandle = CreateThread(NULL,0,myThreadFunc,0,0,&threadID);
Sleep(3000);
cout << "Program Should End Now !\n";
WaitForSingleObject(ThreadhHandle,INFINITE);
return 0;
}اتمنى ان اكون افدتكم و لو بالقليل , مع اعتذر ي المسبق في حال وجود اي خطأ او تقصير, وللمشرفين صلاحية التعديل او التنسيق فضلا عن تصحيح اي خطأ يوجد .
والى دروس قادمة إن شاء الله تعالى اترككم في حفظ الله تعالى
والسلام عليكم و رحمة الله و بركاته

