۱۳۸۹/۰۴/۱۰

انجام پي در پي اعمال Async به كمك Iterators - قسمت اول


تقريبا تمام اعمال كار با شبكه در Silverlight از مدل asynchronous programming پيروي مي‌كنند؛ از فراخواني يك متد وب سرويس تا دريافت اطلاعات از وب و غيره. اگر در ساير فناوري‌هاي موجود در دات نت فريم ورك براي مثال جهت كار با يك وب سرويس هر دو متد همزمان و غيرهمزمان در اختيار برنامه نويس هستند اما اينجا خير. اينجا فقط روش‌هاي غيرهمزمان مرسوم هستند و بس. خيلي هم خوب. يك چارچوب كاري خوب بايد روش استفاده‌ي صحيح از كتابخانه‌هاي موجود را نيز ترويج كند و اين مورد حداقل در Silverlight اتفاق افتاده است.
براي مثال فراخواني‌هاي زير را در نظر بگيريد:
private int n1, n2;

private void FirstCall()
{
Service.GetRandomNumber(10, SecondCall);
}

private void SecondCall(int number)
{
n1 = number;
Service.GetRandomNumber(n1, ThirdCall);
}

private void ThirdCall(int number)
{
n2 = number;
// etc
}
عموما در اعمال Async پس از پايان عمليات در تردي ديگر، يك متد فراخواني مي‌گردد كه به آن callback delegate نيز گفته مي‌شود. براي مثال توسط اين سه متد قصد داريم اطلاعاتي را از يك وب سرويس دريافت و استفاده كنيم. ابتدا FirstCall فراخواني مي‌شود. پس از پايان كار آن به صورت خودكار متد SecondCall فراخواني شده و اين متد نيز يك عمليات Async ديگر را شروع كرده و الي آخر. در نهايت قصد داريم توسط مقادير بازگشت داده شده منطق خاصي را پياده سازي كنيم. همانطور كه مشاهده مي‌كنيد اين اعمال زيبا نيستند! چقدر خوب مي‌شد مانند دوران synchronous programming (!) فراخواني‌هاي اين متدها به صورت ذيل انجام مي‌شد:
private void FetchNumbers()
{
int n1 = Service.GetRandomNumber(10);
int n2 = Service.GetRandomNumber(n1);
}
در برنامه نويسي متداول هميشه عادت داريم كه اعمال به صورت A –> B –> C انجام شوند. اما در Async programming ممكن است ابتدا C انجام شود، سپس A و بعد B يا هر حالت ديگري صرفنظر از تقدم و تاخر آن‌ها در حين معرفي متدهاي مرتبط در يك قطعه كد. همچنين ميزان خوانايي اين نوع كدنويسي نيز مطلوب نيست. مانند مثال اول ذكر شده، يك عمليات به ظاهر ساده به چندين متد منقطع تقسيم شده است. البته به كمك lambda expressions مثال اول را به شكل زير نيز مي‌توان در طي يك متد ارائه داد اما اگر تعداد فراخواني‌ها بيشتر بود چطور؟ همچنين آيا استفاده از عدد n2 بلافاصله پس از عبارت ذكر شده مجاز است؟ آيا عمليات واقعا به پايان رسيده و مقدار مطلوب به آن انتساب داده شده است؟
private void FetchNumbers()
{
int n1, n2;

Service.GetRandomNumber(10, result =>
{
n1 = result;
Service.GetRandomNumber(n1, secondResult =>
{
n2 = secondResult;
});
});
}

به عبارتي مي‌خواهيم كل اعمال انجام شده در متد FetchNumbers هنوز Async باشند (ترد اصلي برنامه را قفل نكنند) اما پي در پي انجام شوند تا مديريت آن‌ها ساده‌تر شوند (هر لحظه دقيقا بدانيم كه كجا هستيم) و همچنين كدهاي توليدي نيز خواناتر باشند.
روش استانداري كه توسط الگوهاي برنامه نويسي براي حل اين مساله پيشنهاد مي‌شود، استفاده از الگوي coroutines است. توسط اين الگو مي‌توان چندين متد Async را در حالت معلق قرار داده و سپس در هر زماني كه نياز به آن‌ها بود عمليات آن‌ها را از سر گرفت.
دات نت فريم ورك حالت ويژه‌اي از coroutines را توسط Iterators پشتيباني مي‌كند (از C# 2.0 به بعد) كه در ابتدا نياز است از ديدگاه اين مساله مروري بر آن‌ها داشته باشيم. مثال بعد يك enumerator را به همراه yield return ارائه داده است:

using System;
using System.Collections.Generic;
using System.Threading;

namespace CoroutinesSample
{
class Program
{
static void printAll()
{
foreach (int x in integerList())
{
Console.WriteLine(x);
}
}

static IEnumerable<int> integerList()
{
yield return 1;
Thread.Sleep(1000);
yield return 2;
yield return 3;
}

static void Main()
{
printAll();
}
}
}

كامپايلر سي شارپ در عمل يك state machine را براي پياده سازي اين عمليات به صورت خودكار توليد خواهد كرد:

private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>2__current = 1;
this.<>1__state = 1;
return true;

case 1:
this.<>1__state = -1;
Thread.Sleep(0x3e8);
this.<>2__current = 2;
this.<>1__state = 2;
return true;

case 2:
this.<>1__state = -1;
this.<>2__current = 3;
this.<>1__state = 3;
return true;

case 3:
this.<>1__state = -1;
break;
}
return false;
}

در حين استفاده از يك IEnumerator ابتدا در وضعيت شيء Current آن قرار خواهيم داشت و تا زمانيكه متد MoveNext آن فراخواني نشود هيچ اتفاق ديگري رخ نخواهد داد. هر بار كه متد MoveNext اين enumerator فرخواني گردد (براي مثال توسط يك حلقه‌ي foreach) اجراي متد integerList ادامه خواهد يافت تا به yield return بعدي برسيم (ساير اعمال تعريف شده در حالت تعليق قرار دارند) و همينطور الي آخر.
از همين قابليت جهت مديريت اعمال Async پي در پي نيز مي‌توان استفاده كرد. State machine فوق تا پايان اولين عمليات تعريف شده صبر مي‌كند تا به yield return برسد. سپس با فراخواني متد MoveNext به عمليات بعدي رهنمون خواهيم شد. به اين صورت ديدگاهي پي در پي از يك سلسه عمليات غيرهمزمان حاصل مي‌گردد.

خوب ما الان نياز به يك كلاس داريم كه بتواند enumerator ايي از اين دست را به صورت خودكار مرحله به مرحله آن هم پس از پايان واقعي عمليات Async قبلي (يا مرحله‌ي قبلي)، اجرا كند. قبل از اختراع چرخ بايد متذكر شد كه ديگران اينكار را انجام داده‌اند و كتابخانه‌هاي رايگان و يا سورس بازي براي اين منظور موجود است.


ادامه دارد ...