Szukaj na tym blogu

środa, 22 września 2010

Sekwencja wywołań w asynchronicznym środowisku Silverlight cz. 2 - CoRutines

W dzisiejszej części przedstawię sposób wykorzystania corutines w Silverlight.
Corutines to mechanizm pozwalający na wykonywanie sekwencji kodu tzn. do puki nie skończy się działanie danego bloku kodu, program nie przejdzie dalej. W Silverlight, gdzie wszystko musi być wywoływane asynchronicznie tego typu mechanizm jest wręcz zbawieniem. Nie oznacza to, że od teraz wszelkiego rodzaju operacje będziesz wykonywał przy wykorzystaniu corutines. To by zabiło całą koncepcję technologii Silverlight, jednakże zdarzają się sytuacje gdzie do wykonania pewnego bloku kodu potrzebujesz dane będące wynikiem operacji poprzedzającego bloku kodu.
Domyślnie język C# nie wspiera corutines. Mechanizm ten można natomiast zbudować z elementów jakie dostarcza nam .NET.
Na rozwiązanie, które chcę zaproponować składa się:
- klasa wątków BackgroundWorker,
- interfejs IEnumerable,
- słówko yield.

Ponieważ większość z nas zetknęła się ze słówkiem yield na samym początku przygody z językiem C# (zapewne czytając jakieś opasłe tomisko) po czym przez resztę swojego komercyjnego / niekomercyjnego doświadczenia nie miała sposobności wykorzystać go (tak, generalizuję), postaram się na prostym przykładzie pokazać jego działanie.

class Program
{
    static void Main(string[] args)
    {
        foreach(var msg in Teksty())
        {
            Console.WriteLine(msg);
        }
        Console.ReadKey();
    }
 
    public static IEnumerable<string> Teksty()
    {
        yield return "Start";
 
        Console.WriteLine("Tutaj sobie coś wykonuję");
        Console.ReadKey();
 
        yield return "Drugi krok";
 
        Console.WriteLine("Kolejne czynności");
        Console.ReadKey();
        yield return "Zakończenie";
    }
}

Wyobraźmy sobie, że iterujemy pętlą foreach po tablicy string’ów { „A”, „B”, „C” }. Czyli każda kolejna iteracja bieże kolejny element tablicy. Dla przedstawionego poniżej przykładu tablica string’ów jest równoważna YieldTab() :

class Program
{
    static void Main(string[] args)
    {
        string[] normalTab = {"A", "B", "C"};
 
        foreach(var msg in normalTab)
        {
            Console.WriteLine(msg);
        }
        Console.ReadKey();
 
        foreach (var msg in YieldTab())
        {
            Console.WriteLine(msg);
        }
        Console.ReadKey();
    }
 
    public static IEnumerable YieldTab()
    {
        yield return "A";
        yield return "B";
        yield return "C";
    }
}


Analizując przykład pierwszy bardzo łatwo wychwycić różnicę. Przy kolejnej iteracji po IEnumerable, program wykonuje wszystko co się znajduje pomiędzy słówkami yield oraz zwraca wartość elementu.
Myślę, że koncepcja działania jest mniej więcej zrozumiała.

Zważając na powyższy przykład opiszę słowno muzycznie jak dziła mechanizm corutines.
1. Tworzymy interfejs pomocniczy IResult

public interface IResult
{
    void Execute();
    event EventHandler Completed;
}

Zadaniem metody Execute() będzie wywołanie dowolnej metody przekazanej to mechanizmu Corutines, natomiast zdarzenie Completed będzie informowało o zakończeniu działania przekazanej metody.

2. Tworzymy klasę implementującą powyższy mechanizm zintegrowany z klasą BackgroundResult

public class BackgroundResult : IResult
{
    private readonly Action action;
    public BackgroundResult(Action action)
    {
        this.action = action;
    }
 
    public void Execute()
    {
        var backgroundWorker = new BackgroundWorker();
        backgroundWorker.DoWork += (e, sender) => action();
        backgroundWorker.RunWorkerCompleted += (e, sender) => Completed(this, EventArgs.Empty);
        backgroundWorker.RunWorkerAsync();
    }
    public event EventHandler Completed;
}


Klasa ta w konstruktorze przyjmuje Action będący delegatem, który nie przyjmuje żadnych parametrów i nie zwraca, żadnej wartości. W skrócie, w konstruktorze przekazujemy referencję do metody odpowiadającej wymaganiom powyższego delegata.

Metoda Execute() uruchamia przekazaną metodę w trybie asynchronicznym i nasłuchuje moment jej zakończenia poprzez zdarzenie RunWorkerCompleted, który następnie wzbudza zdarzenie Completed.

3. Tworzymy klasę, której zadaniem będzie iteracja po IEnumerable w sposób jaki jest właściwy dla opisywanego mechanizmu Corutines.

public class ResultEnumerator
{
    private readonly IEnumerator _enumerator;
    public ResultEnumerator(IEnumerable 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();
    }
}


Klasa przyjmuje w konstruktorze kolekcję elementów implementujących interfejs IResult, a następnie pobiera enumerator tej kolekcji.
MoveNext() enumeratora można przyrównać do pojedyńczego odczytania z pętli np. foreach, gdzie wskazany element znajduje się we właściwości Current. Kolejne wywołanie metody MoveNext() powoduje przesunięcie obecnego elementu o jeden dalej.
Wywołując metodę Enumerate() uruchamiamy swoistą funkcję rekurencyjną ChildCompleted(object sender, EventArgs args), gdzie zakończone zadanie wywołuje ponownie tą samą funkcję.
Proces kończy się w momencie osiągnięcia kresu przekazanej kolekcji.

A tak to działa w praktyce:

public partial class MainPage : UserControl
{
    private AutoResetEvent trigger = new AutoResetEvent(false);
 
    public MainPage()
    {
        InitializeComponent();
        this.Loaded += MainPage_Loaded;
    }
 
    void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        var enumerator = new ResultEnumerator(CallWebServiceMethods());
        enumerator.Enumerate();
    }
    public IEnumerable CallWebServiceMethods()
    {
        yield return new BackgroundResult(() =>
        {
            var serwis = new LocalSerwis.Service1SoapClient();
            serwis.MetodaPierwszaCompleted += (sender, e) =>
            {
                Dispatcher.BeginInvoke(() => MessageBox.Show(e.Result));
                trigger.Set();
            };
            serwis.MetodaPierwszaAsync();
            trigger.WaitOne();
        });
        yield return new BackgroundResult(() =>
        {
            var serwis = new LocalSerwis.Service1SoapClient();
            serwis.MetodaDrugaCompleted += (sender, e) =>
            {
                Dispatcher.BeginInvoke(() => MessageBox.Show(e.Result));
                trigger.Set();
            };
            serwis.MetodaDrugaAsync("Ala ma kota");
            trigger.WaitOne();
        });
    }
}

Gdzie mój serwis ma postać:
public class Service1 : System.Web.Services.WebService
{
    [WebMethod]
    public string MetodaPierwsza()
    {
        return "Wynik Metody Pierwszej";
    }
    [WebMethod]
    public string MetodaDruga(string msg)
    {
        char[] chars = msg.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }
}

Ponieważ zapytania do webserwisu są również asynchroniczne, skorzystałem z klasy AutoResetEvent, która potrafi zastopować aktualny wątek (metoda WaitOne())aż do momentu uruchomienia metody Set(), która go wzbudza.
Wywołanie postaci Dispatcher.BeginInvoke(Action a) pozwala na wykonanie kodu w wątku głównym UI aplikacji Silverlight.

Źródła do przykładu znajdują się pod tym adresem: http://dhd.codeplex.com/releases/view/52747
Na zakończenie chciałbym wspomnieć, że powyższe rozwiązanie nie jest mojego autorstwa tylko Pana Roba Eisenberga, który jest twórcą frameworka MVVM Caliburn (http://caliburn.codeplex.com/).

Brak komentarzy:

Prześlij komentarz