۱۳۸۹/۰۴/۱۱

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


در قسمت قبل ايده‌ي اصلي و مفاهيم مرتبط با استفاده از Iterators مطرح شد. در اين قسمت به يك مثال عملي در اين مورد خواهيم پرداخت.

چندين كتابخانه و كلاس جهت مديريت Coroutines در دات نت تهيه شده كه ليست آن‌ها به شرح زير است:
1) Using C# 2.0 iterators to simplify writing asynchronous code
2) Wintellect's Jeffrey Richter's PowerThreading Library
3) Rob Eisenberg's Build your own MVVM Framework codes

و ...

مورد سوم كه توسط خالق اصلي كتابخانه‌ي Caliburn (يكي از فريم ورك‌هاي مشهور MVVM براي WPF و Silverlight) در كنفرانس MIX 2010 ارائه شد، اين روزها در وبلاگ‌هاي مرتبط بيشتر مورد توجه قرار گرفته و تقريبا به يك روش استاندارد تبديل شده است. اين روش از يك اينترفيس و يك كلاس به شرح زير تشكيل مي‌شود:

using System;

namespace SLAsyncTest.Helper
{
public interface IResult
{
void Execute();
event EventHandler Completed;
}
}

using System;
using System.Collections.Generic;

namespace SLAsyncTest.Helper
{
public class ResultEnumerator
{
private readonly IEnumerator<IResult> _enumerator;

public ResultEnumerator(IEnumerable<IResult> children)
{
_enumerator = children.GetEnumerator();
}

public void Enumerate()
{
childCompleted(null, EventArgs.Empty);
}

private void childCompleted(object sender, EventArgs args)
{
var previous = sender as IResult;

if (previous != null)
previous.Completed -= childCompleted;

if (!_enumerator.MoveNext())
return;

var next = _enumerator.Current;
next.Completed += childCompleted;
next.Execute();
}
}
}

توضيحات:
مطابق توضيحات قسمت قبل، براي مديريت اعمال همزمان به شكلي پي در پي، نياز است تا يك IEnumerable را به همراه yield return در پايان هر مرحله از كار ايجاد كنيم. در اينجا اين IEnumerable را از نوع اينترفيس IResult تعريف خواهيم كرد. متد Execute آن شامل كدهاي عمليات Async خواهند شد و پس از پايان كار رخداد Completed صدا زده مي‌شود. به اين صورت كلاس ResultEnumerator به سادگي مي‌تواند يكي پس از ديگري اعمال Async مورد نظر ما را به صورت متوالي فراخواني نمائيد. با هر بار فراخواني رخداد Completed، متد MoveNext صدا زده شده و يك مرحله به جلو خواهيم رفت.
براي مثال كدهاي ساده WCF Service زير را در نظر بگيريد.

using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Threading;

namespace SLAsyncTest.Web
{
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode
= AspNetCompatibilityRequirementsMode.Allowed)]
public class TestService
{
[OperationContract]
public int GetNumber(int number)
{
Thread.Sleep(2000);//Simulating a log running operation
return number * 2;
}
}
}

قصد داريم در طي دو مرحله متوالي اين WCF Service را در يك برنامه‌ي Silverlight فراخواني كنيم. كدهاي قسمت فراخواني اين سرويس بر اساس پياده سازي اينترفيس IResult به صورت زير درخواهند آمد:

using System;
using SLAsyncTest.Helper;

namespace SLAsyncTest.Model
{
public class GetNumber : IResult
{
public int Result { set; get; }
public bool HasError { set; get; }

private int _num;
public GetNumber(int num)
{
_num = num;
}

#region IResult Members
public void Execute()
{
var srv = new TestServiceReference.TestServiceClient();
srv.GetNumberCompleted += (sender, e) =>
{
if (e.Error == null)
Result = e.Result;
else
HasError = true;

Completed(this, EventArgs.Empty); //run the next IResult
};
srv.GetNumberAsync(_num);
}

public event EventHandler Completed;
#endregion
}
}
در متد Execute كار فراخواني غيرهمزمان WCF Service به صورتي متداول انجام شده و در پايان متد Completed صدا زده مي‌شود. همانطور كه توضيح داده شد، اين فراخواني در كلاس ResultEnumerator ياد شده مورد استفاده قرار مي‌گيرد.
اكنون قسمت‌هاي اصلي كدهاي View Model برنامه به شكل زير خواهند بود:

private void doFetch(object obj)
{
new ResultEnumerator(executeAsyncOps()).Enumerate();
}

private IEnumerable<IResult> executeAsyncOps()
{
FinalResult = 0;
IsBusy = true; //Show BusyIndicator

//Sequential Async Operations
var asyncOp1 = new GetNumber(10);
yield return asyncOp1;

//using the result of the previous step
if(asyncOp1.HasError)
{
IsBusy = false; //Hide BusyIndicator
yield break;
}

var asyncOp2 = new GetNumber(asyncOp1.Result);
yield return asyncOp2;

FinalResult = asyncOp2.Result; //Bind it to the UI

IsBusy = false; //Hide BusyIndicator
}
در اينجا يك IEnumerable از نوع IResult تعريف شده است و در طي دو مرحله‌ي متوالي اما غيرهمزمان كار دريافت اطلاعات از WCF Service صورت مي‌گيرد. ابتدا عدد 10 به WCF Service ارسال مي‌شود و خروجي 20 خواهد بود. سپس اين عدد در مرحله‌ي بعد مجددا به WCF Service ارسال گرديده و حاصل نهايي كه عدد 40 مي‌باشد در اختيار سيستم Binding قرار خواهد گرفت.
اگر از اين روش استفاده نمي‌شد ممكن بود به اين جواب برسيم يا خير. ممكن بود مرحله‌ي دوم ابتدا شروع شود و سپس مرحله‌ي اول رخ دهد. اما با كمك Iterators و yield return به همراه كلاس ResultEnumerator موفق شديم تا عمليات دوم همزمان را در حالت تعليق قرار داده و پس از پايان اولين عمليات غير همزمان، مرحله‌ي بعدي فراخواني را بر اساس مقدار حاصل شده از WCF Service آغاز كنيم.
اين روش براي برنامه‌ نويس‌ها آشناتر است و همان سيستم فراخواني A->B->C را تداعي مي‌كند اما كليه اعمال غيرهمزمان هستند و ترد اصلي برنامه قفل نخواهد شد.

كدهاي كامل اين مثال را از اينجا مي‌توانيد دريافت كنيد.