تقييم الموضوع :
  • 0 أصوات - بمعدل 0
  • 1
  • 2
  • 3
  • 4
  • 5
[مقالة] البرمجة غير المتزامنة Asynchronous (جديد C# 5.0)
#1
كاتب الموضوع : عبد العظيم بخاري

محتويات المقالة :
- مفهوم البرمجة غير المتزامنة.
- حال البرمجة غير المتزامنة قبل C# 5.0.
- البرمجة غير المتزامنة في C# 5.0.
- نظرة اعمق للكلمة المحجوزة await .
- انشاء وتكوين وتنظيم كود غير متزامن ياستعمال TAP.
- البرمجة غير المتزامنة مع multithreading ؟


لم اكد انتهي من الكتابة عن المميزات والخصائص الجديدة في اطار عمل الدوت نت 4 حتى قامت مايكروسوفت في مؤتمر PDC الذي تم التحدث به عن مستقبل لغتي C# والفيجوال بيسك بالإعلان عن احد اهم خصائص الإصدار القادم من لغة C# والذي سوف يحمل الرقم 5.0 (ونفس الشيئ للغة الفيجوال بيسك) .

فبعد أن سحرتنا مايكروسوفت في الإصدار الاخير بالبرمجة المتوازية واتحفتنا بالبرمجة الديناميكية والبرمجة المرنة تعود الان لتأخذ عقولنا إلى عالم البرمجة غير المتزامنة Asynchronous لتؤكد لنا أن مستقبل البرمجيات السريعة والخفيفة هو في اطار عمل الدوت نت وليس في غيره .

ملاحظة : يجب عليك تحميل الإضافة الجديدة للفيجوال ستديو Visual Studio Async CTP حتى تستطيع العمل على الخصائص الجديدة للبرمجة غير المتزامنة :
http://msdn.microsoft.com/en-us/vstudio/async.aspx


فماذا نقصد بالبرمجة غير المتزامنة ؟ , وما التطور الجديد عليها في الإصدار القادم من لغة C# ؟ , وما الفرق بين البرمجة غير المتزامنة والبرمجة المتوازية وكيف يمكن الإستفادة منهم معاً ؟

هذه الأسئلة وغيرها هي ما سنتناوله في هذه المقالة لكن قبل أن ابدأ بها اريد أن انصحك عزيزي القارئ بأن تفهم خصائص البرمجة المتوازية في اطار عمل الدوت نت 4 قبل الخوض في الخصائص الجديدة للغة C# 5.0 وذلك لأن الدمج بين مميزات البرمجة المتوازية وبين البرمجة غير المتزامنة سيعطيك ناتج عظيم , بل وعظيم جداً ايضاً !!. اضافة إلى أن جوهر البرمجة غير المتزامنة يعتمد على كلاس Task فبقدر فهمك له ولطبيعة عمله في البرمجة المتوازية بقدر يمكنك فهم هذه المقالة بالصورة الصحيحة .

مفهوم البرمجة غير المتزامنة

قبل أن نفهم معنى البرمجة غير المتزامنة يجب علينا اولاً أن نفهم معنى البرمجة المتزامنة والتي تمتاز بها اغلب جمل التحكم والتكرار التي تعاملنا معها كلنا في البرامج .

دعونا نلقي نظرة على المثال التالي :


كود :
void StoreDocuments(List<Doc> docs)
{
for(int i = 0; i < docs.Count; i++)
Store(Translate(docs[i]));
}
يقوم الكود السابق ببساطة بعمل ترجمة لكل مستند في القائمة docs (نفترض أنه يحوله من اللغة الإنجليزية إلى اللغة العربية) ويقوم بتخزين المستند المترجم في اي جهاز للتخزين.

بافتراض أن عملية الترجمة تتم عبر API لخدمة Bing أو جووجل على الإنترنت بحيث نقوم بارسال النص لتلك الخدمة اضافة للغات المطلوب الترجمة منها وإليها ثم ترجع لنا النص المترجم ومن ثم نقوم بتخزينه .

يمكننا تلخيص عمل الميثود السابقة كالاتي :

- ناخذ المستند الحالي و نرسله للميثود Transate() والتي تقوم بارسال ذلك المستند لخدمة Bing للترجمة على الإنترنت.
- انتظار النص المترجم من خدمة Bing .
- البدء في تخزين المستند المترجم.
- انتظار انتهاء عملية التخزين (فلنفترض أن جهازالتخزين على سيرفر اخر ويحتاج وقت)
- اعادة نفس العملية حتى ننتهي من ترجمة جميع المستندات .

لاحظ أن هذا البرنامج الصغير مبني على البرمجة العادية المتزامنة , بمعنى اخر يجب علينا انتظار النص المترجم من Bing قبل أن نبدأ عملية التخزين وبعدها علينا انتظار انتهاء عملية التخزين قبل الانتقال لانتظار ترجمة مستند اخر, فلو كان عندنا اتصال ضعيف في الإنترنت فسيكون زمن الإنتهاء من تنفيذ هذا البرنامج كبير جداً خاصة اذا كان عدد المستندات المطلوب معالجتها , حيث أن الCPU سيكون في حالة انتظار ولن يقوم بعمل شيئ لنا اثناء انتظار النص المترجم من خدمة Bing وفي اثناء انتظار تخزين المستند .

هذا الأمر قد يضر كثيراً ببرامجك فستتوقف واجهة برنامجك (UI) عن العمل في اثناء انتظار عملية ما , وسيطول هذا التوقف إن طال زمن الإنتظار .

لاحظ أنه في البرمجة المتزامنة يجب على كل امر أو جملة تحكم أن تنهي تنفيذها قبل الإنتقال إلى الامر التالي .

سيكون برنامجنا رائع في حال قمنا باستغلال فترة الإنتظار ; فاذا قمنا بالسماح للبرنامج بالبدء بتخزين المستند الأول في نفس الوقت التي ننتظر فيه قدوم المستند المترجم الثاني من Bing دون الحاجة لانشاء أي thread جديد سيكون الأداء افضل بكثير.
هذا ما نقصده حقيقة بالبرمجة غير المتزامنة حيث نقوم بمعنى اخر بجعل الميثود الخاصة بالترجمة تعمل بشكل لا متزامن بدلاً من العمل بطبيعتها المتزامنة .

حال البرمجة غير المتزامنة قبل C# 5.0

كانت الطريقة الاكثر قوة لكتابة كودات غير متزامن هي باستعمال اسلوب البرمجة المستمرة Continuation Passing Style او ما نختصره عادة بCPS والذي هو نمط من البرمجة لا يحتوي على subroutines أو returns لكن عوضاً عن ذلك يقوم الفنكشن الذي يعمل حالياً عند نهايته باستدعاء فنشكن اخر ويمرر ناتج الفنكشن الحالي إلى ذلك الفنكشن .

وبما انه لا يوجد في هذا النمط أي فنكشن يرجع قيمة أو يقوم بأي عمل بعد استدعاء الفنكشن الذي بعده فليس هناك حاجة لمعرفة المكان الذي كان فيه التنفيذ قبل الان لأنك فعلياً لن تعود لذلك المكان . وحتى نتأكد أن كل شيئ في في هذا النمط يجري بالترتيب المراد له فيجب علينا أن نمرر "استمرارية" عند استدعائنا للفنكشن والتي هي عبارة عن فنكشن بحد ذاتها يقوم بتنفيذ كل شيئ يأتي بعد الفنكشن الحالي.
نعود الان للمثال الذي وضعناه في القسم السابق ولنفترض أن هناك معجزة ما حدثت وحصلنا على ميثود TranslateAsync والتي تعمل implement للترجمة باستعمال نمط CPS

تقوم هذه الميثود بادارة اللاتزامن بطريقة ما لتنفيذ مهمتها . فيمكن أن تقوم بجلب thread من thread pool ما أو تقوم بتسريع انهاء الthread الحالية المتعلقة بعمليات I/O أو باي طريقة اخرى . فلا نهتم حالياً بذلك . كل ما يهمنا هو أن هذه الميثود :

1- تكون جاهزة لتنفيذ مهمة الترجمة بشكل لا متزامن .
2- عندما تنتهي المهمة غير المتزامنة فإنها تستدعي الاستمرارية المعطاة لهذه الtask.

يجب أن تعلم أن استمرارية عملية ترجمة المستند تأخذ document , ويجب أن تعلم ايضاً أن استدعاء الإستمرارية هو امر مكافئ لارجاع قيمة من استدعاء ميثود وهو الذي نقوم به في البرمجة العادية المتزامنة.

لنبدأ الان باعادة كتابة الكود في القسم السابق بطريقة مماثلة لنمط CPS ومشاهدة ما الذي سوف يحدث.

وحتى نبدأ ذلك يجب عليك أن تعرف أن هذا الأمر صعب ويحتاج لجهد حتى ننهيه بنجاح حيث أننا نحتاج لل مشكلتين رئيسيتين وهما :

- ما هي قيم جميع المتغيرات المحلية عندما نعود لاستكمال التنفيذ بعد اللاتزامن؟ .
- اين تم ايقاف التنفيذ مؤقتاً وكيف حصل ذلك؟.

يمكننا حل المشكلة الاولى عن طريق عمل delegate يحيط بجميع المتغيرات المحلية .

اما المشكلة الثانية فهي نفس المشكلة التي نواجهها مع iterator blocks , وحل هذه المشكلة يكون عبر ايقاف الكنترول بعد yield return . الأمر الذي يجعل الكومبايلر ينشئ كودات state machine وجمل goto للتحكم بالذهاب للتفرعات .

نحن لن نحول الكود بالكامل لنمط CPS لأن ذلك يعني تكلفة عالية جداً في الأداء وهو امر صعب ايضاً عند الكتابة , لذلك سنستخدم نفس الشيئ الذي نستعمله على الiterator blocks .

ودمج حلول هذه المشكلتان معاً سوف يعطينا ميثود لا متزامنة وهو ما نريد تحقيقه في النهاية , لذلك دعونا نصنع lambda لنحيط بالمتغيرات المحلية ونكتب داخلها كود state machine يمثل الإستمرارية بدون كتابة كل شيئ في CPS.

كود :
enum State { Start, AfterTranslate }

void StoreDocuments(List<Doc> docs)
{
State state = State.Start;
int i;
Document document;
Action StoreDocuments = () =>
{
switch(state)
{
case State.Start: goto Start;
case State.AfterTranslate: goto AfterTranslate;
}
Start: ;
for(i = 0; i < docs.Count; i++)
{
state = State.AfterTranslate;
TranslateAsync(docs[i],
resultingDocument=>
{
document = resultingDocument;
StoreDocuments();
});
return;
AfterTranslate: ;
Store(document);
}
};
StoreDocuments();
}
لو دققت في هذا الكود سوف تلاحظ عدة مشاكل فيه , المشكلة الاولى هي اننا نمتلك مشكلة assignment واضحة لأننا نمتلك recursive lambda . لكن لنهمل هذه المشكلة الان.

المشكلة الثانية هي اننا نمتلك انتقال بجملة goto من خارج بلوك إلى داخله وهذا الامر غير مسموح به في C# لكن لنهمل هذه المشكلة ايضاً.

لنبدأ الان بشرح هذا الكود , فلو قام احد ما باستدعاء الميثود StoreDocuments واعطاءها قائمة من المستندات لترجمتها ومن ثم تخزينها فهذا الأمر سوف ينشئ action يقوم بعمل مهمة هذه الميثود ويستدعيها , تقوم بعدها الميثود بارسال الكنترول لتسمية Start وتبدأ TranslateAsync بترجمة المستند الأول بشكل لا متزامن حيث أن اهم امر لهذه الميثود هو أنها تعود مباشرة وتقوم بعملها بشكل غير متزامن.

لا يوجد شيئ اخر يمكننا عمله هنا حيث اننا لا نحتاج لانتظار قدوم المستند المترجم ثم نعمل return مع علمنا أن هذه الميثود سوف تستدعى مرة اخرى لترجمة مستند اخر.

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

بعد ذلك تقوم الإستمرارية باستدعاء الميثود .

تذهب الميثود لتسمية AfterTranslate .(بغض النظر على أن هذا الإنتقال غير شرعي في C#) فنقفز إلى حلقة التكرار ونبدأ من حيث انتهينا .فنخزن المستند بشكل غير متزامن ويستمر تكرار هذه الخطوات .

قمنا لحد الان بخطوة في الاتجاه الصحيح لكننا لم نحقق بعد هدفنا , حيث اننا نريد جعل الخزين للمستند يتم بشكل غير متزامن ايضاً.
سنفعل الان مثل ما فعلنا مع الترجمة , فلنفترض أن هناك ميثود جاءت كالاتي :


كود :
void StoreAsync(Document document, Action continuation)
سنحاول تحويل برنامجنا من اجل الاستفادة من هذه الميثود .

ملاحظة: ميثود Store(document) هي ميثود لا ترجع اي قيمة لذلك الاستمرارية في النسخة غير المتزامنة لن تأخذ اي باراميتر ).


لنحاول الان عمل ذلك عن طريق الكود التالي:


كود :
Action StoreDocuments = () =>
{
switch(state)
{
case State.Start: goto Start;
case State.AfterTranslate: goto AfterTranslate;
case State.AfterStore: goto AfterStore;
}
Start: ;
for(i = 0; i < docs.Count; i++)
{
state = State.AfterTranslate;
TranslateAsync(docs[i], resultingDocument=>
{
document = resultingDocument;
StoreDocuments();
});
return;
AfterTranslate: ;
state = State.AfterStore;
StoreAsync(document, StoreDocuments);
return;
AfterStore: ;
}
};
الذي يحدث الان اننا قمنا بعملية الترجمة بشكل غير متزامن والرجوع بسرعة وعندما تكتمل عملية الترجمة يتم الإنتقال لخطوة التخزين بصورة غير متزامنة ويعود بسرعة , وعندما ينتهي التخزين يتم الإنتقال لاخر لقة التكرار وينتهي الموضوع.

قد تقول في نفسك ما الذي يقوله هذا الشخص وماذا استفدنا من كل هذا الكود حيث انه ما زال سير عمله مشابه للبرمجة المتزامنة العادية.
في الواقع بقي علينا عمل implement للميزة التي نحتاجها وهي تداخل اوقات الإنتظار حيث أن وقت الإنتظار لترجمة المستند الثاني على سبيل المثال يجب أن تتداخل مع وقت الإنتظار لتخزين المستند الاول .

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

المشكلة هنا أننا ما زلنا نعامل StoreAsync كأنها متزامنة حتى نعرف تحديد ما الذي سوف يحدث بعدها . فكل الذي عملناه حتى الان هو تعقيد خط سير عمل هذه الميثود دون أن نضيف حقيقة اي شيئ منطقي فيه . فالذي نحتاجه هو امكانية بدء التخزين بشكل غير متزامن بدون عمل return وبعدها نعمل الامور الاخرى مثل بدء الترجمة التالية.

المعنى من عدم عمل return بسرعة هو أن بداية التزامن لا يجب أن تأخذ استمرارية لأن الشيئ الذي سوف يحصل بعد ذلك هو على وشك الحصول ! لذلك لن نقوم بعملية ارجاع هنا.

لكن ما الذي سوف يأخذ استمرارية اذاً ؟!

لو نظرنا للmethods التي لدينا StoreAsync و TranslateAsync فهما غير كافيتان لما نريد . فلنفترض أن النوع المرجع لTranslateAsync هو AsyncThing<Document> وهو نوع قمت باختراعه ويمثل "تتبع الحالة لعملية غير متزامنة ستقوم في احد الايام بانتاج مستند مترجم" .

هذا هو الشيئ الذي سوف يأخذ استمرارية , ويمكننا الافتراض ايضاً وبشكل مشابه أن الميثود StroreAsync ترجع AsyncThing بدون اي نوع في الargument لأن هذه الميثود لا ترجع اي شيئ بالاصل .

يمكننا عمل الميثود الرئيسية كالاتي :

كود :
AsyncThing<Document> TranslateThing = null;
AsyncThing StoreThing = null;

Action StoreDocuments = () =>
{
switch(state) { blah blah blah }
Start: ;
for(i = 0; i < docs.Count; i++)
{
TranslateThing = TranslateAsync(docs[i]);
state = State.AfterTranslate;
TranslateThing.SetContinuation(StoreDocuments);
return;
AfterTranslate: ;
document = TranslateThing.GetResult();
StoreThing = StoreAsync(document);
state = State.AfterStore;
StoreThing.SetContinuation(StoreDocuments);
return;
AfterStore: ;
}
};
لحد الان الامور جيدة نوعا ما , لكن ليس كما نريد بعد , فنحن الان نمتلك نفس سير العمل كما كان سابقاً .

لقد قمنا بازالة واحدة من الlambda التي كانت متداخلة مع سير العمل لأننا الان يمكننا اخذ المستند عند الTranslate بعد اكتمال الاستمرارية .

لاحظ اننا لسنا بحاجة لامتلاك اليات معينة تجعل الإستمرارية تبدل الحالة وهذا امر جيد .

لكن ماذا عن خاصية مشاركة اوقات الإنتظار بين تخزين المستند السابق وترجمة المستند الحالي؟

الذي نحتاجه هو عدم وضع استمرارية لمهمة التخزين حتى وقت لاحق.

كود :
Start: ;
for(i = 0; i < docs.Count; i++)
{
TranslateThing = TranslateAsync(docs[i]);
state = State.AfterTranslate;
TranslateThing.SetContinuation(StoreDocuments);
return;
AfterTranslate: ;
document = TranslateThing.GetResult();
if (StoreThing != null)
{
state = State.AfterStore;
StoreThing.SetContinuation(StoreDocuments);
return;
AfterStore: ;
}
StoreThing = StoreAsync(document);
}
هل هذا ما نحتاجه حقاً ؟ لنبدأ بشرحه قبل أن نحكم .

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

لاحظ أننا نقوم في هذه النقطة بانتظار انتهاء الترجمة والتخزين في آن واحد . وبعدها ترجع الميثود فوراً بعد وضع الإستمرارية للترجمة وعندما تنتهي ترجمة المستند الثاني سيتم استدعاء الاستمرارية والتي ستنتقل لAfterTranslate وفي هذه اللحظة هناك task Thing للتخزين قد بدأت سابقاً لذلك نضع استمراريتها ونعود فوراً.

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

هل يمكننا عمل شيئ افضل من ذلك ؟

ما الذي سوف يحصل في حال اكتمل تخزين المستند الأول بينما كانالمستند الثاني قيد الترجمة ؟

في هذه الحالة لسنا بحاجة للذهاب إلى كل الكلام الفارغ حول وضع الإستمرارية والعودة فوراً . فالذي علينا القيام به فقط هو استدعاء الاستمرارية بشكل مباشر.

ونفس الأمر لو افترضنا أن TranslateAsync تحتفظ بكاش لعمليات الترجمة على الجهاز نفسه الأمر الذي قد ينهي عملها بشكل مباشر دون الحاجة لاي انتظار أو اي عمليات غير متزامنة.

حتى نعالج هاتين القضيتين فلنفترض أن SetContinuation ترجع bool تعبر عن ما اذا كانت الtask قد اكتملت ام لا . ففي حال كانت القيمة true فإن للtask عمل غير متزامن يجب أن تقوم به اما اذا كانت false فيحدث عكس ذلك.دعنا الان نكتب الكود بصورته النهائية:

كود :
void StoreDocuments(List<Doc> docs)
{
State state = State.Start;
int i;
Document document;
AsyncThing<Document> TranslateThing = null;
AsyncThing StoreThing = null;
Action StoreDocuments = () =>
{
switch(state)
{
case State.Start: goto Start;
case State.AfterTranslate: goto AfterTranslate;
case State.AfterStore: goto AfterStore;
}
Start: ;
for(i = 0; i < docs.Count; i++)
{
TranslateThing = TranslateAsync(docs[i]);
state = State.AfterTranslate;
if (TranslateThing.SetContinuation(StoreDocuments))
return;
AfterTranslate: ;
document = TranslateThing.GetResult();
if (StoreThing != null)
{
state = State.AfterStore;
if (StoreThing.SetContinuation(StoreDocuments))
return;
AfterStore: ;
}
StoreThing = StoreAsync(document);
}
};
StoreDocuments();
}
يمكننا القول الان بأننا قد انتهينا . لكن لو قارنا الكود الاخير مع الكود الذي بدأنا به مشوارنا :



void StoreDocuments(List<Doc> docs){ for(int i = 0; i < docs.Count; i++) Store(Translate(docs[i]));}
سنلاحظ مقدار الصعوبة التي كان يواجهاا المطورون لكتابة كود لا متزامن . ومع اننا قد انهينا الكتابة لبرنامجنا فمازال هذا الكود غير قابل للترجمة بسبب مشاكل التسميات مع النطاق حيث ما زال علينا القيام ببعض الاصلاحات لهذا الكود .

صحيح أن اسلوب CPS قوي جداً لكن كان لا بد من طريقة اسهل وافضل للاستفادة من قوتها بعيداً عن الطريقة التي استعملناها.

يتبع

عبد العظيم بخاري

http://www.el-bukhari.com/2011/01/as...ming-c-50.html
}}}
تم الشكر بواسطة:



التنقل السريع :


يقوم بقرائة الموضوع: بالاضافة الى ( 1 ) ضيف كريم