الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

مفهوم تعدد المسالك في نظام التشغيل Windows - الدرس الاول :

بدأه HaTy في 29 مايو 2009 · 7 رد · 3,130 مشاهدة · في المواضيع والدروس
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع
post-89451-1198550773.gif

السلام عليكــم ورحمـة الله وبركاتــه ،،

اخواني الاعزاء , لطالما استفدت من جهود الاخوة في منتديات الفريق العربي , فلا اقل من ان اشارك في اثراء هذه الجهود الطيبة وإن كان علمي القليل لا يرقى الى مستوى الاعضاء المخضرمين , الا انه يطيب لي ان اشارككم ولو بقدر بسيط مما تعلمته .

اغلب مبرمجي ++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;
}

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

والى دروس قادمة إن شاء الله تعالى اترككم في حفظ الله تعالى

والسلام عليكم و رحمة الله و بركاته

تم تعديل هذه المشاركة بواسطة HaTy في 29 مايو 2009 في 12:45

while( "اسلام" )
{
 cout >> "الموت لبني صهيون" >> endl;
}
return "فلسطين";
#2

السلام عليكم

موضوع رائع أخى بارك الله فيك

يعنى مثلا الكود الآتى

#include <iostream>
using namespace std;
#include <windows.h>
int a=0,b=0;
DWORD WINAPI myThreadFunc1( LPVOID )
{
cout << "Thread Created !" << endl;
for ( int j = 0; j < 10; j++){
Sleep(1000);
	  a+=1;cout<<endl<<" F1 "<<a<<endl;


}
cout << "myThread's Function1 Ended" << endl;
return 0;
}
DWORD WINAPI myThreadFunc2( LPVOID )
{
for ( int j = 0; j < 10; j++){
Sleep(3000);	
	 cout<<endl<<" F2 "<<a<<endl;

}
cout << "myThread's Function2 Ended" << endl;
return 0;
}
int main()
{
DWORD threadID1,threadID2;
HANDLE ThreadhHandle1,ThreadhHandle2;
cout << "Main Function Called\n";
ThreadhHandle1 = CreateThread(NULL,0,myThreadFunc1,0,0,&threadID1);
ThreadhHandle2 = CreateThread(NULL,0,myThreadFunc2,0,0,&threadID2);
cout << "Program Should End Now !\n";
WaitForSingleObject(ThreadhHandle1,INFINITE);
WaitForSingleObject(ThreadhHandle2,INFINITE);
return 0;
}

الدوال الثلاثه main , myThreadFunc1 , myThreadFunc2

تعمل فى نفس الوقت

شكرا لك

اللهم صل على سيدنا محمد وعلى آله وصحبه وسلم

#3

وعليكــم السـلام ورحمة الله وبركاتـه..

نعم اخي أمين , الدوال الثلاث تعمل في نفس الوقت ولكن ...

:: لابد انك لاحظت عند تنفيذ البرنامج تداخل مخرجات الدول , كذلك ظهور قيم غير متوقعة للمتغيرات .

السبب في هذا ان الوصول للمتغيرات Varibales متاح لجميع الدوال , فمثلا لو اردنا ان تنفذ الدالة الاولى عمليات حسابية على احد المتغيرات وقامت الدالة الثانية بتغيير قيمة المتغير قبل انتهاء الدالة الاولى من اكمال عملياتها على ذلك المتغيير فستحصل اخطاء حسابية .

هذا فضلا عن تداخل القيم المطبوعة على شاشة الكنسول لإن الدوال الثلاث تستخدم امر الاخراج cout في نفس الوقت .

لحل هذه الاشكالية يجب الالمام بمفهوم المزامنة بين الاجراءات او ما يعرف بـ Synchronization وتوجد عدة حلول كاستخدام mutex و استخدام Semaphore .

:: وسأحاول اعطاء نبذة عن مفهوم المزامنة Synchronization والتقنيات المستخدمة لمزامنة الاجراءات في دروس قادمة إن شاء الله تعالى

الى ذلكم الحين تقبل تحياتي

والسلام عليكم و رحمة الله و بركاته

تم تعديل هذه المشاركة بواسطة HaTy في 29 مايو 2009 في 13:08

while( "اسلام" )
{
 cout >> "الموت لبني صهيون" >> endl;
}
return "فلسطين";
#4

السلام عليكم ...

درس جميل أخ HaTy, منذ مدة لم نرى دروساً :)

أنا متابع ....

#5

درس مهم في عالم البرمجة

الشرح واضح وجميل

مشكور أخ HaTy

أنا متابع أيضاً

SAFETY TIP : Always delete what you new, and free what you malloc, never mix new with free or malloc with delete.

#6

شرح مرتب و منسق

حقيقى جميل

سأتابع معك اكمل على بركة الله

و الله ولى التوفيق

مدونتي: C++ Tips and Tricks

#7

لابد من اغلاق اي مقبض اذا لم نعد بحاجة اليه ، اذا كان المشروع كبير سيحدث لدينا Handle Leaks .

بالتوفيق أخي :)

mov eax, dword ptr ds:[0xffdf0308]

jmp dword ptr [eax+0xfc]

#8

السلام عليكــم ورحمـة الله وبركاتــه ،،

صدقت اخي GamingMaster لابد من اقفال المقابض بعد انتهاء استخدامها , ولم اتطرق لهذا الامر وساتركه للدرس القادم ان شاء الله تعالى

بعد شرح العمليات او وضائف الثريد , عموما الموضوع متشعب ولكنه بسيط في نفس الوقت , لعل الله يهيىء لنا تغطية الجوانب المهمة فيه بعونه تعالى

تحياتي

while( "اسلام" )
{
 cout >> "الموت لبني صهيون" >> endl;
}
return "فلسطين";

مواضيع مشابهة