الأربعاء، 24 يوليو 2013

كل ما تحب تعرفه عن البرمجة غير المتزامنة Asynchronous


 (جديد C# 5.0) في البرمجة غير المتزامنة 
محتويات المقالة :
مفهوم البرمجة غير المتزامنة.
- حال البرمجة غير المتزامنة قبل 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 قوي جداً لكن كان لا بد من طريقة اسهل وافضل للاستفادة من قوتها بعيداً عن الطريقة التي استعملناها.

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

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

ولو كان عندنا implementation معقول للmethods التي اسماءها TrarnslateAsync و StoreAsync فيمكننا ببساطة تحويل الكود السابق ليحقق الهدف الذي حققناه في القسم الماضي وهو مشاركة اوقات الإنتظار .
async void StoreDocuments(List<Doc> docs)
{
 Task Store = null;
 for(int i = 0; i < docs.Count; i++)
 {
  var document = await TranslateAsync(docs[i]);
  if (Store != null)
   await Store;
  Store = StoreAsync(document);
 }
}

لاحظ اختفاء كودات الstate machine والlambdas وكودات الإستمرارية الذين استعملناهم سابقاً في اكتمال تنفيذ الtasks .
كل هذه الأمور ما زالت موجودة في الواقع لكن التطور الذي حصل أنه بدلاً من كتابتك لهذا كله فلماذا لا تدع الكومبايلر يصنعها عوضاً عنك !.

وبهذا يمكننا كتابة كود غير متزامن في C# 5.0 بطريقة اسهل بكثير من القسم السابق .

يجب أن اوضح مرة اخرى أن ما يقوم به هذا الكود هو نفس ما يقوم به الكود السابق , فكلما كانت task ما في حالة “awaited” سيتم اعتبار بقية الميثود الحالية كاستمرار لتلك الtask وبعدها يتم ارجاع الكنترول للمستدعي فوراً. وعندما يكتمل تنفيذ الtask يتم استدعاء الإستمرارية وبالتالي تبدأ الميثود بالتنفيذ من مكان توقفها اخر مرة .

نظرة متعمقة للكلمة المحجوزة await

قبل أن نخوض معاً بشكل متعمق في اركان البرمجة غير المتزامنة في C# 5.0 يجب أن نوضح امرين مهمين يتعلقان بالكود السابق:
async void StoreDocuments(List<Doc> docs)
{
 Task Store = null;
 for(int i = 0; i < docs.Count; i++)
 {
  var document = await TranslateAsync(docs[i]);
  if (Store != null)
   await Store;
  Store = StoreAsync(document);
 }
}

1- عند وضع الكلمة المجوزة async على الميثود فهذا لا يعني أن هذه الميثود يتم اعتمادها بشكل تلقائي لكي تعمل على thread بشكل غير متزامن . بل إنها في الواقع تعني عكس ذلك , فهذه الميثود تحتوي على جمل تحكم تستدعي عمليات انتظار غير متزامنة وسوف يتم اعادة كتابتها من قبل الكومبايلر إلى نمط CPS للتأكد من أن العمليات غير المتزامنة يمكنها ايقاف هذه الميثود في اللحظة المناسبة.
فبيت القصيد من الكلمة المحجوزة async هو البقاء على الthread الحالية قدر الإمكان حيث أن الmethods الموضوع عليها async تجلب thread متعدد المهام إلى C# .(سنتكلم بشكل متعمق اكثر عن async في الجزء الثاني من هذه المقالة ).

2- استعمال الكلمة المحجوزة await مرتين في تلك الميثود لا يعني أن هذه الميثود تعمل بلوك للthread الحالي حتى تعود المهمة غير المتزامنة وهذا يعني أننا لم نستفد شيئا وستعود المهمة غير المتزامنة إلى مهمة متزامنة وهذا ما نحاول تجنبه .
لذلك هذه الكلمة المحجوزة تعني عكس ذلك ايضاً , فهي تعني أنه اذا لم تنتهي الtask التي ننتظرها بعد سيتم تسجيل بقية هذه الميثود كاستمرار لتلك الtask وبعدها يتم عمل return للمتسدعي بشكل فوري . وستقوم الtask باستدعاء الإستمرارية المسجلة لها عند انتهاء تنفيذها.

قد يخطأ اغلب المبرمجين في فهم معاني الكلمات المحجوزة async و await عندما يشاهدونها للمرة الاولى . فسوف يتبادر لذهنهم عكس المعنى الصحيح , لذلك حاولت مايكروسوفت ايجاد كلمات مجوزة افضل بدلاً من هاتين لكن للاسف لم يتم ايجاد افضلمنهما لحد الان.
اذا كان لديك اي اقتراحات لكلمات مختصرة وسلسة وتعطي المعنى الصحيح ضعها لنا حتى نرسلها لفريق تطوير لغة C# في مايكروسوفت .

ساتحدث الان عن الأمور التي قمنا بكتابتها في القسم الماضي وتم اختصارها في C# 5.0 .
لو نظرنا للسطر البرمجي التالي :
document = await TranslateAsync(docs[i])
فالمقابل له هو :
state = State.AfterTranslate;
TranslateThing = TranslateAsync(docs[i]);
if (TranslateThing.SetContinuation(StoreDocuments))
 return;
AfterTranslate: ;
document = TranslateThing.GetValue();

ففي النمط غير المتزامن الذي لدينا فأي ميثود غير متزامنة سوف ترجع Task<T> بشكل عام , فلنفترض أن TranslateAsync ترجع Task<Document> (سوف اتحدث عن سبب وجود الtask القائمة على نمط العلاقة اللاتزامنية في وقت قريب) فالكود سيكون في الواقع كالاتي :
TranslateAwaiter = TranslateAsync(docs[i]).GetAwaiter();
state = State.AfterTranslate;
if (TranslateAwaiter.BeginAwait(StoreDocuments))
 return;
AfterTranslate: ;
document = TranslateAwaiter.EndAwait();

إن استدعائنا لTranslateAsync سوف ينشأ ويرجع Task<Document> والذي هو عبارة عن object يمثل الtask التي تعمل الان .

إن استدعاء هذه الميثود سوف يرجع فوراً Task<Document> والتي ستقوم بعدها بجلب الdocument بشكل متزامن . فربما تعمل على thread اخر أو اي شيئ , فهذا بالنهاية عملها والذي نعرفه هو أننا بحاجة لشيئ يحصل بعد انتهائها .

وحتى نجعل شيئ ما يحدث عندما تنتهي فإننا نطلب Awaiter للtask والذي يتطلب اثنتين من الmethods . الاولى هي BeginAwait والتي تسجل استمرارية هذه الtask , فيعد أن تنهي الtask مهمتها فإنها تستدعي هذه الإستمرارية ( سندرس كيف يحصل ذلك في موضوع لاحق).

في حال قامت BeginAwait بارجاع true فإنه سيتم استدعاء استمرارية هذه الtask . اما اذا لم ترجع ذلك فإن هذا سيكون بسبب كون هذه الtask انهت عملها وليس هناك حاجة اصلا لاستعمال الية الإستمرار.

الميثود الثانية هي EndAwait والتي تقوم باستخراج الناتج للtask المكتملة .

سوف نحتاج لوضع implementations لكل من BeginAwait و EndAwait على الtask (اللtasks التي لا ترجع شيئ  void return ) و Task<T> (للtasks التي ترجع قيمة ما ).

لكن ماذا عن الasynchronous methods التي لا ترجع object من نوع Task أو Task<T>؟ هنا سوف نستعمل نفس الإستراتيجية التي استعملناها لLINQ فمثلا لو قلنا .
from s in students where s.Name == "Abed" ...
فبالتالي سوف تترجم ل
students.Where(s=>s.Name=="Abed") ...
وسيحاول هذا الناتج ايجاد افضل ميثود لWhere عن طريق الفحص حتى يرى اذا ما كان الstudent يعمل implement لهكذا ميثود ام لا عن طريق الذهاب للextension methods .

تتبع كل من  GetAwaiter وBeginAwait و EndAwait نفس الشيئ تقريباً فسوف نقوم فقط بعمل ناتج overload على الصيغة المحولة ورؤية ما الذي يأتي معه . ويمكننا الذهاب للextension methods في حال احتجنا ذلك .

 لماذا استعملنا Task؟

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

فلا يوجد هناك أي توازي بالأصل في الTask , حيث أن Task Parallel Library تمتلك نمط يعتمد على الtask لتمثيل وحدات العمل المنتظر والتي يمكن تشغيلها بالتوازي , وذلك النمط لا يحتاج إلى multithreaded.

لقد قمت بالإشارة في بعض الأحيان حسب وجهة نظر الكود الذي سنتظر ناتج ما فهو في الواقع لا يهتم اذا ما كان ذلك الناتج تم ايجاده في فترة توقف هذه الthread ام كان في thread عاملة في هذه الprocesses ام كان في process اخرى على هذا الجهاز أو تى لو كان في جهاز تخزين بل وتى لو كان على جهاز اخر موجود في النصف الاخر للكرة الارضية !

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

يمتلك كلاس Task الموجود فيمكتبة TPL الكثير من الأمور التي يمكننا الإستفادة منها , فهو يمتلك تقنية الإلغاء Cancellation اضافة إلى مميزات اخرى مفيدة.

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

تنظيم واعداد المهمات غير المتزامنة 

سنتحدث الان عن تنظيم واعداد المهمات غير المتزامنة ولماذا هذا الأمر معقد في CPS وبسيط جداً مع الكلمة المحجوزة await في C# 5.0 .

سوف ندعو هذا الأمر باسم TAP مبدئياً كاختصار لTask Asynchrony Pattern والذي قد يتغير لاحقاً لاسم اخر .

قد وضعنا المثال الذي استعملناه سابقاً (والمتعلق بترجمة الملفات) خصيصاً لشرح وتبسيط عملية ترتيب اثنتين من المهام اللامتزامنة داخل ميثود لا ترجع اي قيمة , ومع بساطة هذا الأمر إلا أنه عند استعمال نمط CPS فقد كان عمل ذلك صعبا ومعقداً كما رأينا سابقاً.
سنتحدث الان قليلاً عن عملية تنظيم واعداد الmethods غير المتزامنة لكن هذه المرة سيكون موضع دراستنا ميثود يرجع قيمة ما بعكس ما تعاملنا معه سابقاً.

سنفترض أن الميثود ترجع مجموع الbytes التي يتم ترجمتهم من موقع bing مثلاً وبالتالي سيكون شكل الميثود كالاتي في البرمجة المتزامنة الإعتيادية .
long StoreDocuments(List<Doc> docs)
{
  long count = 0;
  for(int i = 0; i < docs.Count; i++)
  {
    var document = Translate(docs[i]);
    count += document.Length;
    Store(document);
  }
  return count;
}

سنعيد الان عملية كتابة اللاتزامن من جديد . ففي ال كانت StoreDocuments سترجع فوراً عند بدء اول TranslateAsync وستتوقف مؤقتاً فقط عند انتهاء اول ترجمة فمتى سيتم تنفيذ "return count;" ؟

لا يمكن للنمط القديم من اللاتزامن لStoreDocuments من ارجاع العدد , فيجب عليه أن يكتب في CPS ايضاً.
void StoreDocumentsAsync(List<Doc> docs, Action<long> continuation)
{
 // يتم التخزين بشكل لا متزامن
 // وبعدها يتم استدعاء الاستمرارية
}

ويحتاج هذا الأمر إلى أن يكون المستدعي لميثود StoreDocumentAsync بأن يكون مكتوب ب CPS حتى يكون بالامكان تمرير الإستمرارية إليه .

لكن ماذا لو تم ارجاع ناتج ما ؟ سيكون هناك  فوضى عندنا حتماً بكل تأكيد .
يمكننا عمل ذلك بسهولة في C# 5.0 عن طريق نموذج TAP بكل بساطة كالاتي :
async Task<long> StoreDocumentsAsync(List<Doc> docs)
{
 long count = 0;
 Task Store = null;
 for(int i = 0; i < docs.Count; i++)
 {
  var document = await TranslateAsync(docs[i]);
  count += document.Length;
  if (Store != null)
   await Store;
  Store = StoreAsync(document);
 }
 return count;
}

سيقوم الكومبايلر بكتابة الكثير من الكودات المعقدة عوضاً عنك حيث سيقوم باضافة التالي :
Task<long> StoreDocuments(List<Doc> docs)
{
 var taskBuilder = AsyncMethodBuilder<long>.Create();
 State state = State.Start;
 TaskAwaiter<Document> TranslateAwaiter = null;
 TaskAwaiter StoreAwaiter = null;
 int i;
 long count = 0;
 Task Store = null;
 Document 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++)
  {
   TranslateAwaiter = TranslateAsync(docs[i]).GetAwaiter();
   state = State.AfterTranslate;
   if (TranslateAwaiter.BeginAwait(StoreDocuments))
    return;
   AfterTranslate:
   document = TranslateAwaiter.EndAwait();
   count += document.Length;
   if (Store != null)
   {
    StoreAwaiter = Store.GetAwaiter();
    state = State.AfterStore;
    if (StoreAwaiter.BeginAwait(StoreDocuments))
     return;
    AfterStore:
    StoreAwaiter.EndAwait();
   }
   Store = StoreAsync(document);
  }
  taskBuilder.SetResult(count);
  return;
 };
 StoreDocuments();
 return taskBuilder.Task;
}



(يجب أن تعلم أنه ما زال هناك بعض المشاكل بالتسميات التي تكون خارج النطاق , حيث أن الكومبايلر لا يحتاج لاتباع قوانين السورس كود للغة C# عند توليد هذه الكودات نيابة عنك . لذلك يفترض أن العلامات الموجودة مشابهة لجملة goto .

اعلم ايضاً أننا ما زلنا لا نمتلك اي معالجة للاستثناءات هنا . وكما هي معالجة الإستثناءات في CPS معقدة وغريبة الأطوار لأن هناك اثنتين من الإستمرارية : الإستمرارية العادية واستمرارية الخطأ , فكيف يمكن التعامل مع هذا الأمر ؟ هذا ما سنناقشه في قسم معالجة الإستثناءات في TAP وذلك في الجزء الثاني من هذه المقالة ) .

ساوضح الان ما الذي يحدث في الكود السابق . لنأخذ الحالة البديهية , ففي حال كانت الlist فارغة فما الذي سوف يحدث ؟  سوف نقوم بانشاء task builder وسننشئ delegate لا يرجع شيئ وبعدها نستدعي هذا الdelegate بشكل متزامن والذي سوف يقوم بتهيئة المتغير الخارجي count إلى صفر .

وبعدها نذهب إلى التسمية Start ونتجاهل حلقة التكرار ونخبر الميثود المساعدة بأنه يوجد لديها ناتج فتتم عملية الإرجاع .
بعد هذا يكون الdelegate قد انجز عمله . فنطلب task من الtask builder والذي يعرف أن عمل الtask انتهى فيرجع completed task تمثل ببساطة الرقم صفر .

في حال حاول المستدعي انتظار الtask فسوف يقوم الawaiter الذي بها بارجاع false عندما يطلب منه بدء عمليات async, لأن الtasks انتهت . اما في حال لم ينتظر المستدعي الtask بعدها . ففي هذه الحالة سوف يتعامل معها كأي task اخرى وفي النهاية يمكنه طلب الناتج منها أو اهمالها في حال لم يلقي لها شأناً .

لندرس الان الحالة الصعبة وهي عندما يكون هناك عدة مستندات يجب أن تعالج , فإننا ننشئ task builder و delegate يتم استدعاؤه بشكل متزامن مثل الحالة السابقة . سوف نبدأ بعدها بمعالجة غير متزامنة اول مرة خلال حلقة التكرار ونسجل الdelegate كاستمرارية لها , وبعدها نرجع من الdelegate وفي تلك اللحظة يقوم task builder ببناء task معينة تكون حالتها أنها تعمل بشكل غير متزامن على الميثود StoreDocumentsAsync ويقوم بارجاع تلك الtask . وعندما تنتهي Translate task بشكل غير متزامن وتستدعي الإستمرارية التي وقفت عندها سابقاً عن طريق سحر الstate machine .

كل شيئ يعمل ويتتابع ويستمر وفق هذه الحلقة التي تختلف عن النسخة التي لا ترجع اي قيمة بفرق وحيد وهو ارجاع Task<long> لاشارات StoreDocumentsAsync والتي تنتهي (عن طريق استدعاء استمراريتها ) عندما يغير الdelegate الtask builder بوضع الناتج .

قبل أن اضيف بعض الامور على اعداد وتظيم الtasks سنذكر ملاحظة سريعة على الextensibility للTAP . فقد تم تصميم LINQ لتكون قابلة للتطبيق بشكل كبير حيث أن اي نوع يعمل implement ل Select و Where ...الخ أو يمتلك extensions لهم فيمكن أن يستعمل تقنيات الإستعلام في LINQ.

نفس الشيئ مع TAP حيث أن اي نوع يمتلك GetAwaiter ويرجع نوع يمتلك BeginAwait و EndAwait ..الخ , فيمكن استعمالهم في تعابير await .

يجب أن تعلم أن الmethods الموضوع عليها الكلمة المحجوزة async يمكنها فقط ارجاع void أو Task أو Task<T> لبعض الانواع في T.

تعمل مايكروسوفت الان على تفعيل الextensibility لTA لاستعمالها في الأنواع اللامتزامنة الموجودة في C# . لكن ليس هناك رغبة حالياً لتفعيل هذا الأمر على الانواع الخارجية .

اذا كنت منتبهاً على ما نقول فسوف تلاحظ إلى انني لم اناقش نقاط الextensibility  لtask builder وهذا ما سوف افعله في وقت لاحق .
نعود الان لنكمل موضوعنا في هذا القسم .

يوجد هناك في LINQ بعض الحالات تكون فيها استعمالات هذه "اللغة" مثل جملة Where طبيعية جداً وتستعمل نمط كتابي سلس . مثل ((Where(B=>..).

نفس الامر هو مع TAP فهدفنا هو في الحقيقة استعمال نمط بسيط من C# لاعداد وتنظيم وتنسيق المهام غير المتزامنة . لذلك اصبح عندنا الان جمل مثل WhenAll و WhenAny والتي تعمل اعداد للtasks كالاتي
List<List<Doc>> groupsOfDocs = whatever;
  Task<long[]> allResults = Task.WhenAll(from docs in groupsOfDocs select StoreDocumentsAsync(docs));
  long[] results = await allResults;


تقوم StoreDocumentsAsync في هذا الكود بارجاع Task<long> لذلك يتم ارجاع الاستعلام IEnumerable<Task<long>> .
تأخذ WhenAll سلسلة من الtasks وتنتج task جديدة تنتظر كل منهم بشكل غير متزامن وضع الناتج في array وبعدها تستدعي الإستمرارية الخاصة مع الناتج عندما يكون متوفر .

نفس الشيئ عندما يكون عندنا WhenAny والتي تأخذ سلسلة من الtasks وتنتج task جديدة تقوم باستدعاء استمراريتها مع الناتج الأول عند اكتمال اي من تلك الtasks .

(لكن ماذا لو انتهت الtask الاولى بشكل ناجح وقامت بقية الtasks برمي استثناء .. هذا الأمر سنناقشه في الجزء الثاني من هذه المقالة).

سيكون لدينا عدة اوامر اخرى اضافة لعدة methods في الإصدار النهائي لC# 5.0 .

يمكنك رؤية امثلة CTP من اجل رؤية امثلة اكثر عن هذا الموضوع .

يجب أن تعلم أن في هذا الإصدار من CTP لم تقم مايكروسوفت بتعديل كلاس Task الموجود منذ الإصدار الرابع , لكن قامت باضافة الأوامر السابقة ل TaskEx . لكن سوف يتم في الإصدار الأخير ل.NET Framework 5.0 نقل كل هذه الاوامر لكلاس Task.

البرمجة غير المتزامنة مع multithreading

سنتحدث الان حول اللاتزامن وعدم استدعاءه لأي multithreading مطلقاً .
قد يتبادر إلى ذهنك عزيزي القارئ سؤال حول كيفية عمل لاتزامن دون multithreading ؟ هذا سؤال غريب حقاً لكن دعنا نعيد صياغته بصورة اخرى كالاتي :

كيف يمكن عمل multitasking بدون وجود عدة CPUs؟

فلا يمكنك عمل امرين في نفس الوقت في ال كان هناك CPU واحد . فيجب أن تعلم أن الmultitasking على معالج واحد تعني ببساطة أن نظام التشغيل يوقف task معينة ويحفظ استمراريتها (تكملتها) في مكان ما وينتقل لtask اخرى ويشغلها لفترة ما ويحفظ تكملتها في مكان ما وهكذا ..

وفي النهاية ينتقل التنفيذ ويعود للtask الاولى لذلك لا يوجد اي Concurrency في نظام المعالج الواحد .
فلا يمكن أن تكون هناك حالة ما يقوم فيها المعالج بتنفيذ امرين معاً في نفس الوقت .

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

لمزيد من التفاصيل حول البرمجة غير المتزامنة في لغتي C# 5.0 و VB.NET 5.0 يمكنكم قراءة هذه الWhitepaper من مايكروسوفت.

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=d7ccfefa-123a-40e5-8ed5-8d2edd68acf4
منقوووووووووووووول
عبد العظيم بخاري.-منتديات ستار تايمز

ليست هناك تعليقات:

إرسال تعليق