Friday, March 03, 2006

Threading in .net

بسم الله الرحمن الرحيم


هل جربت من قبل أن تتعامل مع الThreads ؟؟؟؟
اذا كانت اجابتك بلا فاقرأ معي هذا الموضوع و اذا كانت اجابتك بنعم فقرأه أيضاً ففيه فائدة عظيمة ستجدها بين سطور الموضوع

هل ترى التعامل مع الThreads صعب ؟؟؟؟
هو ليس صعباً و لكنه فقط مرحلة أخرى متقدمة و لا يجب أن تكون كل مرحلة متقدمة صعبة و العكس صحيح فأحيانا البداية تكون الأصعب على الاطلاق

ما هي الThreads ؟
ال
Threads هي محاولة تجزئة البرنامج الى مجموعة من العمليات المستقلة و التي يمكن أداءها بطريقة متوازية Parallel بدلاً من اضاعة الوقت في انتظار لا طائل منه و هو النظام المتسلسل Serial Execution و يهدف النظام الى تحسين أداء البرامج عموماً فاذا تخيلنا مثلاً برنامجأ رياضياً يقوم بعمل حسابات طويلة و معقدة (Matlab مثلاً) فان البرنامج أثناء تنفيذ هذه العمليات لا يمكنه اداء اي مهمة اخرى حتى ينتهي من الحسابات و من مساوئ هذا هو عدم استجابة البرنامج الى المستخدم (بالبلدي الشاشة بتهنج)...

و لكن هل هذا مانراه فعلاً في البرامج الحقيقية؟؟؟
بالطبع لا فبرنامج
Matlab مثلاً يمكتك من تشغيل برامجك و كتابة برامج أخرى و التفاعل مع واجهاته في نفس الوقت بدون أي مشاكل و هذا لأن جميع هذه العمليات هي عمليات مستقلة يمكن التعامل معها من خلال البرنامج بطريقة متوازية

هل ال
Threading مفهوم جديد
ال
Threading بصفة عامة مفهوم معروف و لكن للأسف لم يتم تطبيقه على مستوى لغات البرمجة بل تُرك مفتوحاً لكل نظام يطبقه كما يشاء و يدعم ما يريد و هذا ما جعل الThreading في ال++C مثلاً من المواضيع السيئة فكل نظام له الAPI's الخاصة به و التي تختلف كلياً عن أي نظام اخر...
و لكن مع ظهور الأجيال الحديثة من لغات البرمجة بدأً من ال
JAVA و التي حسب معلوماتي أول لغة ظهر الThreading فيها كجزء من اللغة نفسها بحيث أنتقلت مشاكل التعامل مع النظام و طريقة تطبيق الThreading نفسها الى الJVM بدلاً من المبرمج المسكين...
و بالطبع مع ظهور ال.
net كان الThreading جزء لا يتجزأ من الFramework و يمكن استخدامه من أي لغة تتبع الCLR ..

اذاً كيف نقوم بعمل Threading في ال#C ؟؟؟
أولاً هناك
Namespace تختص بالThreading و هي System.Threading و هي تحتوي على جميع الclasses الخاصة بالThreading....
اذاً فأول سطر في برنامج يتعامل مع ال
Threading هو

CODE

using System.Threading


أهم
Class في الThreading بالطبع هي System.Threading.Thread و هي التي توجد فيها أغلب العمليات الأساسية
اذاً هل الخطوة الثانية هي انشاء
object من نوع Thread ؟؟
الحقيقة هذه الخطوة تسبقها خطوة قبلها و هي كتابة ال
function التي ستقوم هذه الThread بتنفيذها و لتكن مثلاً

CODE

void PrintHello()
{
Console.WriteLine("Hello Threading...");
}


ثم نقوم بعمل ال
Object من نوع Thread

CODE

Thread thread = new Thread(new ThreadStart(PrintHello));


ثانية واحدة ايه اللي انت كاتبه ده ؟؟؟؟ مين ThreadStart دي و ازاي كاتب اسم الfunction كأنه parameter ؟؟؟؟
لو سألت نفسك السؤال السابق اذن فأنت أول مرة تتعامل مع ال
Delegates و هي باختصار شديد بديل محترم لما كنا نستخدمه في ال++C/C و هو الFunction Pointer ...
الحقيقة المجال لا يتسع لشرح ال
Delegate هنا و لكن يمكنك أن ترجع للMSDN أو ابحث في المنتدى... و لو مستعجل كل اللي محتاج تعرفه في السطر اللي فات اني بقول للThread بتاعتي ان الfunction اللي حتنفذها اسمها PrintHello
شفت خرجتني من الموضوع ازاي؟؟؟
mad.gifعلى العموم ما علينا

بقيت خطوة أخرى و هي تشغيل ال
Thread نفسها باضافة سطر اخر

CODE

thread.Start();



هي دي الThreads بقة و عاملين الهوليلا دي على ال 3 سطور دول؟
لأ طبعاً دي البداية بس الموضوع طويل و فيه كلام كبير صحصح معايا كده لسة التقيل ورا

طيب هل هذه هي الطريقة الوحيدة لانشاء Threads

الاجابة بالطبع لا و لكن توجد طرق أخرى كثيرة و هناك أيضاً طرق يتم فيها انشاء
Threads بواسطة النظام دون أن يدخل المبرمج في تفاصيل انشاء الThreads و هي ما يسمى بالAsynchronous Operations و ستجد الكثير منها في الdotnet فمبدئيأ أي عملية تبدأ بكلمة BeginْXXX و يوجد لها مثيل يبدأ بEndXXX تعتبر عمليات غير متزامنه و مثال على هذا في الStreams ستجد BeginRead و EndRead و أيضاً BeginWrite و EndWrite و هذا موضوع اخر مثير في الdotnet و سأحاول شرحه لاحقاً
و يوجد أيضاً طريقة أخرى لانشاء ال
Threads و تعتمد على فكرة أن انشاء Thread جديدة و تشغيلها و مايتطلبه هذا من بعض العمليات الأخرى أحياناً يكون وقت هذه العمليات أكبر من العملية التي ستنفذها الThread في النهاية مما يؤثر سلباً على الأداء و هنا ظهر مفهوم الThreadPool أي مستودع الThreads و هي مجموعة من الThreads المنشأة (بضم الميم) مسبقاً و هي جاهزة و تحتاج فقط للعملية التي ستقوم بها و يكون لها عدد مُحدد و يمكن للمستخدم أن يستخدمها ببساطة جداً
طريقة استخدامها تكون من ال
ThreadPool Class عن طريق وضع الfunction المُراد تنفيذها في الQueue حتى تفرغ احدى الThreads لتنفيذ هذه العملية و يكون هكذا

CODE

ThreadPool.QueueUserWorkItem(new WaitCallback(PrintHello));


و هكذا سيتم وضع العملية في ال
Queue و سيكمل البرنامج عمله دون انتظار و عندما تكون هناك Thread جاهزة سيتم التنفيذ

مشكلة كبيرة جداً تواجه البرامج التي تعمل فيها أكثر من Thread و هي مشكلة ال Data Race أو التسابق...
شرح هذه المشكلة بسيط
انظر معي الى هذا الكود

CODE

using System;
using System.Threading;

public class Test
{
static int count=0;

static void Main()
{
ThreadStart job = new ThreadStart(ThreadJob);
Thread thread = new Thread(job);
thread.Start();

for (int i=0; i < 5; i++)
{
count++;
}

thread.Join();
Console.WriteLine ("Final count: {0}", count);
}

static void ThreadJob()
{
for (int i=0; i < 5; i++)
{
count++;
}
}
}


في الكود السابق ستلاحظ وجود متغير عام داخل ال
class و هو متغير integer اسمه count و هو عبارة عن عداد و يمثل بالنسبة لنا Shared Resource أو متغير مشترك...
ستجد أيضاً انه تم تعريف
Thread و تم اعلامها بالfunction الخاصة بها و هي عمل increment خمس مرات للمتغير count و في نفس الوقت تقوم الThread الرئيسية بعمل عملية مشابهة داخل الMain و هكذا أصبح لدينا 2 Threads يقوم كل منهم بعمل Increment خمس مرات بمجموع نهائي 10 حتى يصبح الناتج النهائي (نظرياً) 10 و لكن الواقع أن ال 2 Threads يتصارعان على نفس الShared Resource و هذه هي مشكلة الData Race ..
أين المشكلة...
المشكلة أنه يمكن ان يحدث
Overlapping أو تداخل بين عمليات القراءة و الكتابة المتوازية فتخيل معي أن الThread الرئيسية قد قرأت القيمة في المتغير count ( و لتكن 0 ) و قامت بعمل increment و قبل أن تقوم بكتابة القيمة الجديدة في المتغير count كانت الThread الأخرى قرأت القيمة القديمة (القيمة 0) و بدأت العمل عليها ثم جاءت الthread الرئيسية و كتبت القيمة الجديدة (القيمة 1) و هنا كانت الThread الجديدة قد أنهت عملها و جاءت لتخزن القيمة الجديدة بعد الincrement و هي (القيمة 1) !!!!!!!!!!!
هكذا أصبح هناك خطأ لأنه نظرياً حدثت عمليتي
increment للرقم صفر و لكن القيمة النهائية أصبحت 1 و هكذا لن تكون القيمة النهائية للبرنامج 10 كما هو متوقع !!!!!
مش مصدقني جرب الكود اللي فات .....
جربته؟؟؟
حتيجي تقوللي أنا جربت الكود و طلعت النتيجة النهائية 10 يعني كلامك مش صحيح...؟؟؟؟
لأ الكلام صحيح و لكن لكي تفهم ماحدث يجب أولاً أن نفهم كيف يُعامل نظام التشغيل ال
Threads ...
يقوم نظام التشغيل بمنح ال
Thread وقتاً محدداً على الProcessor و يسمى Round و ليكن زمنها T و تقوم الthread بأداء عملياتها و هنا يحدث حدث من 3 احداث لتتوقف الThread عن استخدام الProcessor
الأول هو أن تنتهي الThread من عملها قبل انتهاء الزمن T و هنا يقوم نظام التشغيل بازالة الThread و وضع أخرى مكانها
الثاتي هو أن ينتهي الزمن T قبل أن تنتهي الThread من عملها و هنا يقوم النظام بعمل Block للThread و حفظ حالتها و وضع Thread أخرى مكانها الى أن يأتي دور الThread الأولى مرة أخرى فيقوم النظام بعمل UnBlock و اعادتها الى حالتها لتستكمل عملها
الحدث الثالث هو أن يقوم أي من النظام أو المستخدم بعمل Abort للThread و انهاء عملها.. أو عمل Sleep و هنا سيعاملها النظام كأنها قد أنهت الفترة المتاحة لها T و سيتم عمل Block لها الى ان يأتي دورها مرة أخرى

اذن ما الذي جعل الكود السابق -الذي نعتبره نظرياً خطأ - يعمل بشكل صحيح ؟؟؟
السبب هو أن ال
Thread قصيرة جداً فهي تقوم بعملية بسيطة و هذا ماجعلها تستغرق وقتاً أقل من T فلم تحدث مشاكل الData Race و لكن يمكن أن تظهر المشكلة في جهاز اخر ابطأ أو تكون فيه T أقل و هذه تعتبر نقطة ضعف في الكود
يمكننا أن نرى هذه المشاكل باضافة بعض السطور المعطلة للكود هكذا

CODE

using System;
using System.Threading;

public class Test
{
static int count=0;

static void Main()
{
ThreadStart job = new ThreadStart(ThreadJob);
Thread thread = new Thread(job);
thread.Start();

for (int i=0; i < 5; i++)
{
int tmp = count;
Console.WriteLine ("Read count={0}", tmp);
Thread.Sleep(50);
tmp++;
Console.WriteLine ("Incremented tmp to {0}", tmp);
Thread.Sleep(20);
count = tmp;
Console.WriteLine ("Written count={0}", tmp);
Thread.Sleep(30);
}

thread.Join();
Console.WriteLine ("Final count: {0}", count);
}

static void ThreadJob()
{
for (int i=0; i < 5; i++)
{
int tmp = count;
Console.WriteLine ("\t\t\t\tRead count={0}", tmp);
Thread.Sleep(20);
tmp++;
Console.WriteLine ("\t\t\t\tIncremented tmp to {0}", tmp);
Thread.Sleep(10);
count = tmp;
Console.WriteLine ("\t\t\t\tWritten count={0}", tmp);
Thread.Sleep(40);
}
}
}


ستجد أننا ملأنا الكود بتعليمات
Thread.Sleep في أماكن مدروسة بين القراءة و التعديل و الكتابة حتى تظهر المشكلة و ستجد أن الناتج النهائي أصبح 6 و ليس 10 !!!!!!
اذن ماهو الحل لهذه المشكلة؟؟؟؟
الحل علمياً استخدام ال
Mutual Exclusion أو مايطلق عليه اختصاراً الMutex و يطلق على الفكرة عموماً Monitors و تعتمد الفكرة على فكرة عزل المناطق التي يُحتمل حدوث هذه المشكلة فيها و هي مناطق التعامل مع الShared Resources و جعل هذه المناطق تعمل بطريقة غير متوازية ...
لكي تفهم فكرتها تخيل أننا وضعنا باباً له قفل على هذه المناطق بمجرد أن تدخل
Thread من الباب تغلق الباب خلفها حتى تنتهي قم تخرج و تفتح القفل و أثناء وجودها في الداخل لا تستطيع اي Thread أخرى أن تدخل ....
و لاستخدام هذه الخاصية يمكننا استخدام ال
class Monitor بوضع Monitor.Enter و Monitor.Exit حول هذه المناطق..
أو يمكننا استخدام تعليمة أخرى و هي
lock هكذا

CODE

using System;
using System.Threading;

public class Test
{
static int count = 0;
static readonly object countLock = new object();

static void Main()
{
ThreadStart job = new ThreadStart(ThreadJob);
Thread thread = new Thread(job);
thread.Start();

for (int i = 0; i < 5; i++)
{
lock(countLock)
{
int tmp = count;
Console.WriteLine("Read count={0}", tmp);
Thread.Sleep(50);
tmp++;
Console.WriteLine("Incremented tmp to {0}", tmp);
Thread.Sleep(20);
count = tmp;
Console.WriteLine("Written count={0}", tmp);
}
Thread.Sleep(30);
}

thread.Join();
Console.WriteLine("Final count: {0}", count);
}

static void ThreadJob()
{
for (int i = 0; i < 5; i++)
{
lock (countLock)
{
int tmp = count;
Console.WriteLine("\t\t\t\tRead count={0}", tmp);
Thread.Sleep(20);
tmp++;
Console.WriteLine("\t\t\t\tIncremented tmp to {0}", tmp);
Thread.Sleep(10);
count = tmp;
Console.WriteLine("\t\t\t\tWritten count={0}", tmp);
}
Thread.Sleep(40);
}
}
}


لاحظ أننا عرفنا متغير من نوع
object و هو يمثل هنا القفل و استخدمنا نفس المتغير مع كل عملية lock تتعلق بنفس الshared resource و هكذا فانك عند تنفيذ الكود السابق فان النتيجة ستكون دائماً 10
و الى اللقاء قريباً في عرض لمشاكل ال
Threads مع الGUI

مشاكل الThreading مع الGUI Controls و المشكلة الأساسية و هي في هذا المثال
لو فرضنا أننا نملك
Form تحتوي على Label اسمه lblTest و سنقوم بتشغيل Thread لتغيير قيمة الText في هذا الLabel
هل هذه مشكلة؟؟؟؟
أعتقد أن الكثير منا سيشمر عن أكمامه و يكتب كود مشابه لهذا الكود

CODE


public void InitThread()
{
Thread thread = new Thread(new ThreadStart(ChangeText));
thread.Start();
}

private void ChangeText()
{
this.lblTest.Text = "Changed";
}


و لكن عند تنفيذ هذا الكود سنفاجأ بتوقف البرنامج عن العمل و ظهور
Exception يوضح لنا أنه لا يُمكن للThread الجديدة أن تُعدل في أي Controls أنشأتها الParent Thread !!!!!!!!!!!!

هل هذا يعني أنه لا يمكن للChild Thread أن تغير في أي Control على الForm ؟
بالطبع لا, يمكن لل
thread أن تقوم بتغيير الControls و لكن قبل أن نشرح الطريقة يجب أولاً أن نُعطي نبذة صغيرة عن
Windows Message Queue & Message Pumping
ببساطة
عندما تقوم بانشاء و تشغيل أي مشروع من أي نوع فان هناك ما يُسمى ال
Main Thread و هي الthread التي تقوم بتنفيذ ال Main function و هي التي تتولى تنفيذ العمليات الموجودة في البرنامج ...
و لكن
طبيعة البرامج ذات الواجهات الرسومية
GUI Application مثل Windows Forms تتميز بأن المستخدم يستطيع أن يتفاعل مع الForm أو البرنامج بشكل عام و في نفس الوقت يقوم البرنامج بتنفيذ هذه الأوامر بالاضافة الى الحفاظ على شكل الForm أي أنه يقوم برسمها مرة بعد أخرى ...
هنا يقوم ال
Windows بتحويل جميع العمليات و الأوامر على الForm الى ما يُسمى Messages و يضعها بترتيب حدوثها في الMessage Queue حتى يتم تنفيذها و يتم تنفيذ هذه العملية عن طريق الMessage Pumping بأن يقوم البرنامج بعد التشغيل بتشغيل Module يقوم بعمل loop على جميع الMessages داخل الMessage Queue و ينفذ الأوامر المتعلقة بكل Message مثل الضغط على زر أو تحريك المؤشر و هكذا يمكننا تخيل عملية الMessage Pumping كأنها على هذا الشكل

CODE


Message message = null;
while ((message = GetNextMessage()) != null)
{
ProcessMessage(message);
}


و هكذا يستمر البرنامج في العمل حتى تنتهي كل ال
Messages أو تأتي Message لانهاء البرنامج نفسه ...
و هنا نسأل أين تُنفذ عملية ال Message Pumping ؟؟؟؟
الجواب بسيط في ال
Main Thread و الدليل أنه اذا تسببت احد الMessages التي يستغرق تنفيذها بعض الوقت الى تجمد الشاشة و هذا لأن الMain Thread مشغولة بتنفيذ الMessage و لا يوجد لديها وقت لاعادة رسم الForm ....

سؤال اخر متى يتم تشغيل الMessage Pumping ؟؟؟
يتم تشغيل ال
Message Pumping مع تنفيذ هذه الinstruction الموجودة في كل Windows App و هي

CODE

Application.Run(new AnyFormYouWant());

داخل الMain

و الان بعد أن أخذنا نبذة عن ال
Message Queueing دعونا نغوص أكثر داخل الWindows Messaging in Win32
ماذا كنا نفعل أيام الWin32 API's لتغيير خاصية في Form أو أي شئ من هذا القبيل؟؟
كنا نملك 2
API's و هما
SendMessage
PostMessage
و كلٍ منهما له استخدام أمثل و هنا يجب أن نعرف الفرق بينهما أولاً
SendMessage
تقوم بوضع
Message جديدة داخل الMessageQueue و تنتظر الى أن تقوم عملية الMessage Pumping بتنفيذ هذه الMessage حتى تنتهي [Blocking Send] و هي الطريقة الأنسب لتغيير الخواص من ال Main Thread و لكنها ستفشل عند استخدامها من Thread أخرى لتغيير خواص Control أنشأه الMain Thread
PostMessage
تقوم أيضاً بوضع
Message جديدة داخل الMessage Queue و لكنها لا تنتظر حتى ينتهي تنفيذ الMessage أو حتى يبدأ التنفيذ فهي تنتهي بمجرد وصول الMessage الى الQueue و هذا يُسمى [Non Blocking Send] و هي تصلح في حالة الChild Thread تريد تغيير خواص في Control أنشأه الMain Thread لأنها أشبه بترك رسالة من الChild Thread الى الMain Thread حتى تقوم الMain Thread أثناء عملية الPumping بتنفيذها.. اي في النهاية الMain Thread هي التي قامت بالتغيير

و الان نعود مرة أخرى الى ال
Dot Net
ماذا يحدث عندما يتم تنفيذ عملية مثل
myForm.Height = 100;
يقوم ال
JIT Compiler بتحويل هذا الأمر الى SendMessage و يتم عندها ارسال Message لتغيير طول الForm و توضع في الQueue حتى يتم تنفيذها في الMessage Pumping و هذه العملية تنجح فقط اذا كانت الMain Thread و التي أنشأت الForm منذ البداية هي التي قامت بعمل تغيير الخاصية...
و لكن ماذا اذا أردت أن استخدم Post Message ؟؟؟؟ هل يمكنني ذلك؟
بالطبع يمكنك ذلك عن طريق استخدام
BeginInvoke أو Invoke الموجودتين في كل الControls و في هذه الحالة يتم ارسال Message الى الQueue و لا ننتظر انتهاء تنفيذها فوراً... و هذا هو ما نحتاجة حتى تستطيع الChild Threads تغيير خصائص الForm أو الControls

و الآن نعود الى نفس ال
Code الذي كتبناه في أول المقال و الذي أردنا به تغيير Label Text باستخدام Thread أخرى...


CODE


private delegate void ChangeDelegate();
public void InitThread()
{
Thread thread = new Thread(new ThreadStart(ChangeText));
thread.Start();
}

private void ChangeText()
{
if(this.InvokeRequired)
{
this.BeginInvoke(new ChangeDelegate(ChangeText), null);
return;
}
this.lblTest.Text = "Changed";
}


و الان بعض التعديلات ظهرت على الكود و في هذه المرة الكود سيعمل طبيعياً و سأشرح ماتم تعديله
قبل الشرح هناك نقطة صغيرة...
كيف أستطيع أن أحدد متى سأستخدم BeginInvoke و متى لن أستخدمها ؟؟
الجواب بسيط توجد داخل كل
Control خاصية Property تدعى InvokeRequired و هي من نوع bool و تكون قيمتها ب False اذا كان الMain Thread هو الذي بقوم بتنفيذ InvokeRequired و تكون بtrue عندما تكون أي Thread أخرى ...
اذاً يمكننا الان فهم الكود السابق ببساطة فكل الفكرة فيه تكمن في ال
if condition التي تستشف من يقوم بتنفيذ هذا الكود فإذا كان الMain Thread فسيتم تغيير الText باستخدام الText Property مباشرة أما اذا كانت thread اخرى فسيتم ارسال رسالة بواسطة PostMessage من خلال BeginInvoke ووضعها في الMessageQueue و هنا عندما يأتي دور الMain Thread في التنفيذ فانها ستقوم بتغيير الText بنفسها

1 comment:

mohammed said...

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