Scale Asynchronous Client-Server Links with Reactive
On 25 tammikuun, 2022 by admin- 31.01.2019
- 17 minutes to read
Kesäkuu 2016
Volume 31 Number 6
Tekijä: Peter Vogel | Kesäkuu 2016
Asynkroninen käsittely on yleistynyt sovelluskehityksessä, Microsoftin .NET Framework on hankkinut laajan valikoiman työkaluja, jotka tukevat tiettyjä asynkronisia suunnittelumalleja. Usein hyvin suunnitellun asynkronisen sovelluksen luominen on kiinni siitä, että tunnistetaan suunnittelumalli, jonka sovelluksesi toteuttaa, ja valitaan sitten oikea joukko .NET-komponentteja.
Jossain tapauksissa ottelu edellyttää useiden .NET-komponenttien integrointia. Stephen Clearyn artikkeli ”Patterns for Asynchronous MVVM Applications: Commands” (bit.ly/233Kocr), osoittaa, miten Model-View-ViewModel (MVVM) -mallia voidaan tukea täysin asynkronisesti. Muissa tapauksissa tuki vaatii vain yhden .NET Frameworkin komponentin. Olen käsitellyt provider/consumers-kuvion toteuttamista BlockingCollectionin avulla VisualStudioMagazine.com Practical .NET -palstoillani ”Create Simple, Reliable Asynchronous Apps with BlockingCollection” (bit.ly/1TuOpE6) ja ”Create Sophisticated Asynchronous Applications with BlockingCollection” (bit.ly/1SpYyD4).
Toiseksi esimerkiksi voidaan mainita observer-suunnittelun toteuttaminen, jolla voidaan valvoa pitkäaikaista toimintoa asynkronisesti. Tässä skenaariossa asynkroninen metodi, joka palauttaa yhden Task-olion, ei toimi, koska asiakas palauttaa usein tulosvirran. Näissä skenaarioissa voit hyödyntää ainakin kahta .NET Frameworkin työkalua: ObservableCollectionia ja Reactive Extensionsia (Rx). Yksinkertaisiin ratkaisuihin tarvitaan vain ObservableCollection (sekä async- ja await-avainsanat). ”Mielenkiintoisemmissa” ja erityisesti tapahtumapohjaisissa ongelmissa Rx tarjoaa kuitenkin paremman hallinnan prosessiin.
Mallin määrittely
Vaikka observer-mallia käytetään usein käyttöliittymäsuunnittelumalleissa – kuten Model-View-Controller (MVC), Model-View-Presenter (MVP) ja MVVM – UI:itä tulisi pitää vain yhtenä skenaariona suuremmasta joukosta skenaarioita, joissa observer-mallia sovelletaan. Tarkkailija-mallin määritelmä (lainaus Wikipediasta) on seuraava: ”Objekti, jota kutsutaan subjektiksi, ylläpitää luetteloa riippuvaisista objekteistaan, joita kutsutaan tarkkailijoiksi, ja ilmoittaa niille automaattisesti kaikista tilamuutoksista, yleensä kutsumalla jotakin niiden metodeista.”
Todellisuudessa tarkkailijakuviossa on kyse siitä, että pitkäaikaisten prosessien tulokset saadaan asiakkaalle heti, kun ne ovat saatavilla. Ilman jotakin versiota observer-kuviosta asiakkaiden on odotettava, kunnes viimeinen tulos on saatavilla, ja sitten kaikki tulokset lähetetään heille yhtenä könttänä. Yhä asynkronisemmassa maailmassa havainnoitsijoiden halutaan käsittelevän tuloksia rinnakkain asiakkaan kanssa sitä mukaa, kun tuloksia on saatavilla. Korostaakseni, että havainnointimallia hyödynnettäessä puhutaan muustakin kuin käyttöliittymistä, käytän loppuosassa ”havainnoitsijan” ja ”kohteen” sijasta sanoja ”asiakas” ja ”palvelin”.
Ongelmat ja mahdollisuudet
Vavainnointimalliin liittyy ainakin kolme ongelmaa ja kaksi mahdollisuutta. Ensimmäinen ongelma on lapsed-listener-ongelma: Monet observer-kuvion toteutukset vaativat, että palvelin pitää hallussaan viittausta kaikkiin asiakkaisiinsa. Tämän seurauksena palvelin voi pitää asiakkaita muistissa, kunnes palvelin poistuu. Tämä ei tietenkään ole optimaalinen ratkaisu pitkään käynnissä olevalle prosessille dynaamisessa järjestelmässä, jossa asiakkaat kytkeytyvät ja katkaisevat yhteyksiä usein.
Lappeutunut kuuntelija -ongelma on kuitenkin vain oire toisesta, suuremmasta ongelmasta: monet havainnointimallin toteutukset vaativat palvelimen ja asiakkaan olevan tiukasti kytkettyjä, jolloin sekä palvelimen että asiakkaan on oltava läsnä koko ajan. Asiakkaan pitäisi ainakin pystyä määrittämään, onko palvelin paikalla ja päättää olla liittymättä; lisäksi palvelimen pitäisi pystyä toimimaan, vaikka tuloksia vastaanottavia asiakkaita ei olisikaan.
Kolmas ongelma liittyy suorituskykyyn: Kuinka kauan kestää, että palvelin ilmoittaa kaikille asiakkaille? Suorituskykyyn tarkkailijamallissa vaikuttaa suoraan ilmoitettavien asiakkaiden määrä. Siksi observer-kuviossa on mahdollisuus parantaa suorituskykyä antamalla asiakkaan suodattaa palvelimelta takaisin tulevat tulokset ennakoivasti. Näin voidaan ratkaista myös tilanteet, joissa palvelin tuottaa enemmän tuloksia (tai useampia erilaisia tuloksia) kuin asiakas on kiinnostunut: Asiakas voi ilmoittaa, että se saa ilmoituksen vain tietyissä tapauksissa. Toinen suorituskykymahdollisuus liittyy sen tunnistamiseen, kun palvelimella ei ole tuloksia tai kun se on lopettanut tulosten tuottamisen. Asiakkaat voivat ohittaa palvelimen tapahtumien käsittelyyn tarvittavien resurssien hankkimisen, kunnes asiakkaalle on taattu, että on jotain käsiteltävää, ja asiakkaat voivat vapauttaa nämä resurssit heti, kun tietävät, että viimeinen tulos on käsitelty.
Tarkkailijasta Publish/Subscribe-malliin
Tämä huomioiminen johtaa yksinkertaisista tarkkailijamallin toteutuksista siihen liittyvään publish/subscribe-malliin. Publish/subscribe toteuttaa observer-mallin löyhästi kytketyllä tavalla, jonka avulla palvelimet ja asiakkaat voivat suorittaa toimintoja, vaikka toinen ei olisi juuri nyt käytettävissä. Publish/subscribe toteuttaa tyypillisesti myös asiakaspuolen suodatuksen antamalla asiakkaan tilata joko tiettyjä aiheita/kanavia (”Ilmoita minulle ostotilauksista”) tai erityyppiseen sisältöön liittyviä attribuutteja (”Ilmoita minulle kaikista kiireellisistä pyynnöistä”).
Yksi ongelma on kuitenkin edelleen olemassa. Kaikilla tarkkailijamallin toteutuksilla on taipumus sitoa asiakkaat ja palvelimet tiukasti tiettyyn sanomamuotoon. Viestin muodon muuttaminen useimmissa publish/subscribe-toteutuksissa voi olla hankalaa, koska kaikki asiakkaat on päivitettävä käyttämään uutta muotoa.
Tämä on monella tapaa samankaltainen kuin tietokannan palvelinpuolen kursorin kuvaus. Siirtokustannusten minimoimiseksi tietokantapalvelin ei palauta tuloksia, kun jokainen rivi haetaan. Suurten rivijoukkojen kohdalla tietokanta ei kuitenkaan myöskään palauta kaikkia rivejä yhtenä eränä lopussa. Sen sijaan tietokantapalvelin palauttaa tyypillisesti osajoukkoja palvelimella pidetystä kursorista usein sitä mukaa, kun nämä osajoukot tulevat saataville. Tietokannassa asiakkaan ja palvelimen ei tarvitse olla samanaikaisesti läsnä: Tietokantapalvelin voi toimia, kun asiakkaita ei ole paikalla; asiakas voi tarkistaa, onko palvelin käytettävissä, ja jos ei ole, päättää, mitä (jos mitään) muuta se voi tehdä. Suodatusprosessi (SQL) on myös hyvin joustava. Jos tietokantamoottori kuitenkin muuttaa muotoa, jota se käyttää rivien palauttamiseen, kaikki asiakkaat on ainakin käännettävä uudelleen.
Objektien välimuistin käsittely
Tapaustutkimukseni yksinkertaisen havainnointimallin toteutuksen tarkastelemiseksi käytän palvelimena luokkaa, joka hakee laskuja muistissa olevasta välimuistista. Tämä palvelin voisi käsittelynsä päätteeksi palauttaa kokoelman kaikista laskuista. Haluaisin kuitenkin mieluummin, että asiakas käsittelee laskut yksitellen ja rinnakkain palvelimen hakuprosessin kanssa. Tämä tarkoittaa, että pidän parempana versiota prosessista, joka palauttaa jokaisen laskun sitä mukaa kuin se löytyy ja antaa asiakkaan käsitellä jokaista laskua rinnakkain seuraavan laskun haun kanssa.
Yksinkertainen toteutus palvelimesta saattaisi näyttää tältä:
private List<Invoice> foundInvoices = new List<Invoice>();public List<Invoice> FindInvoices(decimal Amount){ foundInvoices.Clear(); Invoice inv; // ...search logic to add invoices to the collection foundInvoices.Add(inv); // ...repeat until all invoices found return foundInvoices;}
Kehittyneemmät ratkaisut saattaisivat käyttää yield returnia palauttaakseen jokaisen laskun sitä mukaa kuin se on löytynyt sen sijaan, että kokoaisivat listan. Siitä huolimatta asiakas, joka kutsuu FindInvoices-metodia, haluaa suorittaa joitakin kriittisiä toimintoja ennen ja jälkeen käsittelyn. Kun esimerkiksi ensimmäinen kohde on löydetty, asiakas saattaa haluta ottaa käyttöön MatchingInvoices-luettelon laskujen säilyttämiseksi asiakkaalla tai hankkia/initialisoida kaikki laskun käsittelyyn tarvittavat resurssit. Kun uusia laskuja lisätään, asiakkaan on käsiteltävä jokainen lasku, ja kun palvelin ilmoittaa, että viimeinen lasku on haettu, vapautettava kaikki resurssit, joita ei enää tarvita, koska käsiteltäviä laskuja ei enää ole.
Tietokannan haun aikana lukeminen estyy esimerkiksi siihen asti, kunnes ensimmäinen rivi palautetaan. Kun ensimmäinen rivi on palautettu, asiakas alustaa kaikki resurssit, joita tarvitaan rivin käsittelyyn. Read palauttaa myös false:n, kun viimeinen rivi on haettu, jolloin asiakas voi vapauttaa kyseiset resurssit, koska käsiteltäviä rivejä ei enää ole.
Yksinkertaisten ratkaisujen luominen ObservableCollectionin avulla
Näyttävin valinta havainnointimallin toteuttamiseen .NET Frameworkissa on ObservableCollection. ObservableCollection ilmoittaa asiakkaalle (tapahtuman kautta) aina, kun se on muuttunut.
Esimerkkipalvelimeni uudelleenkirjoittaminen ObservableCollection-luokan käyttöön vaatii vain kaksi muutosta. Ensinnäkin tulokset sisältävä kokoelma on määriteltävä ObservableCollectioniksi ja tehtävä julkiseksi. Toiseksi, metodin ei enää tarvitse palauttaa tulosta: Palvelimen tarvitsee vain lisätä laskuja kokoelmaan.
Palvelimen uusi toteutus voi näyttää tältä:
public List<Invoice> FindInvoices(decimal Amount){ public ObservableCollection<Invoice> foundInvoices = new ObservableCollection<Invoice>(); public void FindInvoices(decimal Amount) { foundInvoices.Clear(); Invoice inv; // ...search logic to set inv foundInvoices.Add(inv); // ...repeat until all invoices are added to the collection }
Palvelimen tätä versiota käyttävän asiakkaan tarvitsee vain kytkeä tapahtumankäsittelijä InvoiceManagementin foundInvoices-kokoelman CollectionChanged-tapahtumaan. Seuraavassa koodissa olen laittanut luokan toteuttamaan IDisposable-rajapinnan tukeakseni tapahtumasta irrottautumista:
public class SearchInvoices: IDisposable{ InvoiceManagement invMgmt = new InvoiceManagement(); public void SearchInvoices() { invMgmt.foundInvoices.CollectionChanged += InvoicesFound; } public void Dispose() { invMgmt.foundInvoices.CollectionChanged -= InvoicesChanged; }
Clientissä CollectionChanged-tapahtumalle välitetään toisena parametrina NotifyCollectionChangedEventArgs-olio. Kyseisen objektin Action-ominaisuus määrittää sekä sen, mikä muutos kokoelmaan tehtiin (toiminnot ovat: kokoelma tyhjennettiin, kokoelmaan lisättiin uusia kohteita, olemassa olevia kohteita siirrettiin/korvattiin/poistettiin) että tiedot muuttuneista kohteista (kokoelma kaikista lisätyistä kohteista, kokoelma kohteista, jotka olivat kokoelmassa ennen uusien kohteiden lisäämistä, siirretyn/poistetun/korvatun kohteen sijainti).
Yksinkertainen asiakaskoodi, joka käsittelisi asynkronisesti jokaisen laskun sitä mukaa, kun se lisätään palvelimen kokoelmaan, näyttäisi kuvassa 1 esitetyn koodin kaltaiselta.
Kuva 1 Laskujen asynkroninen käsittely ObservableCollectionin avulla
private async void InvoicesFound(object sender, NotifyCollectionChangedEventArgs e){ switch (e.Action) { case NotifyCollectionChangedAction.Reset: { // ...initial item processing return; } case NotifyCollectionChangedAction.Add: { foreach (Invoice inv in e.NewItems) { await HandleInvoiceAsync(inv); } return; } }}
Vaikka tämä koodi on yksinkertaista, se voi olla tarpeisiisi riittämätön, varsinkin jos käsittelet pitkäkestoista prosessia tai työskentelet dynaamisessa ympäristössä. Asynkronisen suunnittelun kannalta koodi voisi esimerkiksi kaapata HandleInvoiceAsync:n palauttaman Task-olion, jotta asiakas voisi hallita asynkronisia tehtäviä. Kannattaa myös varmistaa, että CollectionChanged-tapahtuma herätetään UI-säikeessä, vaikka FindInvoices suoritettaisiinkin taustasäikeessä.
Koska Clear-metodia kutsutaan palvelinluokassa juuri siinä kohdassa, jossa sitä kutsutaan (juuri ennen ensimmäisen laskun etsimistä), Action-ominaisuuden Reset-arvoa voidaan käyttää signaalina siitä, että ensimmäinen kohde on pian haettu. Etsinnässä ei kuitenkaan välttämättä löydy yhtään laskua, joten Reset Action -ominaisuuden käyttäminen saattaa johtaa siihen, että asiakas varaa resursseja, joita ei todellisuudessa koskaan käytetä. ”Ensimmäisen erän” käsittelyä varten sinun pitäisi lisätä Add Action -käsittelyyn lippu, joka suoritetaan vain silloin, kun ensimmäinen erä on löydetty.
Palvelimella on lisäksi rajallinen määrä vaihtoehtoja osoittaa, että viimeinen lasku on löydetty, jotta asiakas voi lopettaa ”seuraavan” odottamisen. Palvelin voisi oletettavasti tyhjentää kokoelman sen jälkeen, kun viimeinen kohde on löydetty, mutta se vain pakottaisi monimutkaisemman käsittelyn Reset Action -käsittelyyn (olenko käsitellyt laskuja? Jos kyllä, olen käsitellyt viimeisen laskun; jos en, olen käsittelemässä ensimmäistä laskua).
Vaikka yksinkertaisiin ongelmiin ObservableCollection sopii hyvin, mikä tahansa kohtuullisen monimutkainen ObservableCollectioniin perustuva toteutus (ja mikä tahansa sovellus, joka arvostaa tehokkuutta) vaatii monimutkaista koodia, erityisesti clientissä.
Rx:n ratkaisut
Jos haluat asynkronista käsittelyä, Rx (saatavilla NuGetin kautta) voi tarjota paremman ratkaisun observer-mallin toteuttamiseen lainaamalla publish/subscribe-mallia. Tämä ratkaisu tarjoaa myös LINQ-pohjaisen suodatusmallin, paremman signaloinnin ensimmäisen/viimeisen kohteen ehdoille ja paremman virheenkäsittelyn.
Rx pystyy myös käsittelemään mielenkiintoisempia observer-toteutuksia kuin mitä ObservableCollectionilla on mahdollista. Tapaustutkimuksessani palvelin saattaa alkuperäisen laskujen luettelon palauttamisen jälkeen jatkaa uusien laskujen tarkistamista, jotka on lisätty välimuistiin alkuperäisen haun päätyttyä (ja jotka tietysti vastaavat hakuehtoja). Kun kriteerit täyttävä lasku ilmestyy, asiakkaalle halutaan ilmoittaa tapahtumasta, jotta uusi lasku voidaan lisätä luetteloon. Rx tukee tällaisia tapahtumapohjaisia laajennuksia observer-malliin paremmin kuin ObservableCollection.
Rx:ssä on kaksi keskeistä rajapintaa observer-mallin tukemiseksi. Ensimmäinen on IObservable<T>, jonka toteuttaa palvelin ja joka määrittää yhden metodin: Subscribe. Subscribe-metodin toteuttava palvelin saa asiakkaalta viittauksen objektiin. Vanhentuneen kuuntelijan ongelman käsittelemiseksi Subscribe-metodi palauttaa asiakkaalle viittauksen objektiin, joka toteuttaa IDisposable-rajapinnan. Asiakas voi käyttää tätä objektia katkaistakseen yhteyden palvelimeen. Kun asiakas katkaisee yhteyden, palvelimen odotetaan poistavan asiakkaan kaikista sisäisistä listoistaan.
Toisena on IObserver<T>-rajapinta, joka asiakkaan on toteutettava. Tämä rajapinta edellyttää, että asiakas toteuttaa ja paljastaa palvelimelle kolme metodia: OnNext, OnCompleted ja OnError. Kriittinen metodi on OnNext, jota palvelin käyttää välittääkseen viestin asiakkaalle (tapaustutkimuksessani tämä viesti olisi uusia laskuobjekteja, jotka palautetaan, kun kukin niistä ilmestyy). Palvelin voi käyttää asiakkaan OnCompleted-metodia ilmoittaakseen, että tietoja ei ole enää jäljellä. Kolmas metodi, OnError, tarjoaa palvelimelle tavan ilmoittaa asiakkaalle, että on tapahtunut poikkeus.
Olet tietysti tervetullut toteuttamaan IObserver-rajapinnan itse (se on osa .NET Frameworkia). Yhdessä ObservableCollectionin kanssa se voi olla kaikki mitä tarvitset, jos luot synkronista ratkaisua (olen kirjoittanut siitäkin kolumnin ”Writing Cleaner Code with Reactive Extensions” ).
Rx sisältää kuitenkin useita paketteja, jotka tarjoavat näiden rajapintojen asynkronisia toteutuksia, mukaan lukien toteutukset JavaScriptille ja RESTful-palveluille. Rx Subject -luokka tarjoaa IObservable-luokan toteutuksen, joka yksinkertaistaa observer-mallin asynkronisen publish/subscribe-version toteuttamista.
Asynkronisen ratkaisun luominen
Palvelimen luominen Subject-olion kanssa työskentelyä varten vaatii hyvin vähän muutoksia alkuperäiseen synkroniseen palvelinpuolen koodiin. Korvaan vanhan ObservableCollectionin Subject-oliolla, joka välittää jokaisen laskun sellaisena kuin se näkyy kaikille kuunteleville asiakkaille. Julistan Subject-olion julkiseksi, jotta asiakkaat voivat käyttää sitä:
public class InvoiceManagement{ public IObservable<Invoice> foundInvoice = new Subject<Invoice>();
Metodin rungossa sen sijaan, että lisäisin laskun kokoelmaan, käytän Subject-olion OnNext-metodia välittääkseni jokaisen laskun asiakkaalle sitä mukaa, kun se löytyy:
public void FindInvoices(decimal Amount){ inv = GetInvoicesForAmount(Amount) // Poll for invoices foundInvoice.OnNext(inv); // ...repeat...}
Clientissäni julistan ensin palvelinluokan instanssin. Sitten kutsun asynkiksi merkityssä metodissa Subjectin Subscribe-metodia osoittaakseni, että haluan alkaa hakea viestejä:
public class InvoiceManagementTests{ InvoiceManagement invMgmt = new InvoiceManagement(); public async void ProcessInvoices() { invMgmt.foundInvoice.Subscribe<Invoice>();
Suodattaakseni tulokset vain haluamiini laskuihin voin soveltaa Subject-olioon LINQ-lauseen. Tämä esimerkki suodattaa laskut niihin laskuihin, jotka ovat takautuvasti tilattuja (Rx LINQ-laajennusten käyttämiseksi sinun on lisättävä using-lause System.Reactive.Linq-nimiavaruudelle):
invMgmt.foundInvoice.Where(i => i.BackOrder == "BackOrder").Subscribe();
Kun olen aloittanut Subjectin kuuntelun, voin määritellä, mitä käsittelyä haluan tehdä, kun saan laskun. Voin esimerkiksi käyttää FirstAsync-toimintoa käsitelläkseni vain ensimmäisen palvelun palauttaman laskun. Tässä esimerkissä käytän await-lauseketta FirstAsync-kutsun kanssa, jotta voin palauttaa hallinnan sovellukseni päärunkoon laskun käsittelyn aikana. Tämä koodi odottaa ensimmäisen laskun hakemista, siirtyy sitten siihen koodiin, jota käytän laskun käsittelyprosessin alustamiseen, ja lopuksi käsittelee laskun:
Invoice inv;inv = await invMgmt.foundInvoice.FirstAsync();// ...setup code invoices...HandleInvoiceAsync(inv);
Yksi varoitus: FirstAsync estää, jos palvelin ei ole vielä tuottanut tuloksia. Jos haluat välttää estämisen, voit käyttää FirstOrDefaultAsyncia, joka palauttaa nollan, jos palvelin ei ole tuottanut tuloksia. Jos tuloksia ei ole, asiakas voi päättää, mitä tekee, jos tekee mitään.
Tyypillisempi tapaus on, että asiakas haluaa käsitellä kaikki palautetut laskut (suodatuksen jälkeen) ja tehdä sen asynkronisesti. Siinä tapauksessa voit Subscribe- ja OnNext-menetelmien yhdistelmän sijaan käyttää vain ForEachAsync-metodia. Voit välittää metodin tai lambda-lausekkeen, joka käsittelee saapuvat tulokset. Jos annat metodin (joka ei voi olla asynkroninen), kuten tässä, kyseiselle metodille välitetään ForEachAsyncin käynnistänyt lasku:
invMgmt.foundInvoice.ForEachAsync(HandleInvoice);
ForEachAsync-metodille voidaan välittää myös peruutusmerkki, jonka avulla asiakas voi viestittää, että se katkaisee yhteyden. Hyvä käytäntö olisi välittää merkki kutsuttaessa mitä tahansa Rx *Async-metodia, jotta voidaan tukea sitä, että asiakas voi lopettaa käsittelyn ilman, että hänen tarvitsee odottaa kaikkien objektien käsittelyä.
ForEachAsync ei käsittele yhtään tulosta, jota on jo käsitelty First- (tai FirstOrDefaultAsync-) metodilla, joten voit käyttää FirstOrDefaultAsyncia ForEachAsyncin kanssa tarkistaaksesi, onko palvelimella mitään käsiteltävää ennen kuin käsittelet seuraavia objekteja. Kohteen IsEmpty-metodi suorittaa kuitenkin saman tarkistuksen yksinkertaisemmin. Jos asiakkaan on varattava tulosten käsittelyyn tarvittavia resursseja, IsEmpty-menetelmän avulla asiakas voi tarkistaa, onko jotain käsiteltävää ennen resurssien varaamista (vaihtoehtoisesti resurssit voitaisiin varata silmukan ensimmäisen käsiteltävän kohteen yhteydessä). Käyttämällä IsEmpty:tä asiakkaan kanssa, joka tarkistaa, onko tuloksia olemassa ennen resurssien allokointia (ja käsittelyn aloittamista) ja samalla tukee peruutusta, saataisiin koodia, joka näyttää jotakuinkin kuvion 2 kaltaiselta.
Kuvio 2 Koodi, joka tukee peruutusta ja lykkää käsittelyä siihen asti, kunnes tulokset ovat valmiit
CancellationTokenSource cancelSource = new CancellationTokenSource();CancellationToken cancel;cancel = cancelSource.Token;if (!await invMgmt.foundInvoice.IsEmpty()){ // ...setup code for processing invoices... try { invMgmt.foundInvoice.ForEachAsync(HandleInvoice, cancel); } catch (Exception ex) { if (ex.GetType() != typeof(CancellationToken)) { // ...report message } } // ...clean up code when all invoices are processed or client disconnects}
Pakkaaminen
Jos tarvitset vain yksinkertaisen toteutuksen tarkkailijakuviollektiivista (observer pattern), ObservableCollection saattaa riittää kaikkeen, mitä tarvitset tulosteen virtauksen käsittelyssä. Parempaa hallintaa ja tapahtumapohjaista sovellusta varten Subject-luokka ja Rx:n mukana tulevat laajennukset antavat sovelluksellesi mahdollisuuden työskennellä asynkronisessa tilassa tukemalla tehokasta publish/subscribe-mallin toteutusta (enkä ole vielä tutustunut Rx:n mukana tulevaan runsaaseen operaattorikirjastoon). Jos työskentelet Rx:n kanssa, kannattaa ladata Rx:n suunnitteluopas (bit.ly/1VOPxGS), jossa käsitellään parhaita käytäntöjä havainnoitavien virtojen kuluttamisessa ja tuottamisessa.
Rx tarjoaa myös jonkin verran tukea asiakkaan ja palvelimen välillä välitettävän sanomatyypin muuntamiseen ISubject<TSource, TResult> -rajapinnan avulla. ISubject<TSource, TResult> -rajapinta määrittelee kaksi tietotyyppiä: ”in”-tietotyypin ja ”out”-tietotyypin. Tämän rajapinnan toteuttavassa Subject-luokassa voidaan suorittaa kaikki tarvittavat operaatiot palvelimelta palautetun tuloksen (in-tietotyyppi) muuntamiseksi asiakkaan tarvitsemaksi tulokseksi (out-tietotyyppi). Lisäksi in-parametri on kovariantti (se hyväksyy määritellyn tietotyypin tai mitä tahansa, josta tietotyyppi periytyy) ja out-parametri on kontravariantti (se hyväksyy määritellyn tietotyypin tai mitä tahansa, joka periytyy siitä), mikä antaa lisää joustavuutta.
Elämme yhä enenevässä määrin asynkronisessa maailmassa, ja siinä maailmassa observer-malli tulee entistä tärkeämmäksi – se on käyttökelpoinen apuväline missä tahansa prosessien välisessä rajapinnoissa, joissa palvelinprosessi palauttaa useamman kuin vain yhden tuloksen. Onneksi .NET Frameworkissa on useita vaihtoehtoja observer-kuvion toteuttamiseen, kuten ObservableCollection ja Rx.
Peter Vogel on järjestelmäarkkitehti ja PH&V Information Servicesin pääjohtaja. PH&V tarjoaa täysimittaista konsultointia UX-suunnittelusta objektimallinnukseen ja tietokantasuunnitteluun.
Kiitos seuraaville Microsoftin teknisille asiantuntijoille tämän artikkelin tarkistamisesta: Stephen Cleary, James McCaffrey ja Dave Sexton
Stephen Cleary on työskennellyt monisäikeistyksen ja asynkronisen ohjelmoinnin parissa 16 vuotta, ja hän on käyttänyt Microsoft .NET Frameworkin asynkronista tukea ensimmäisestä yhteisöllisestä teknologiakatselmuksesta lähtien. Hän on kirjoittanut kirjan ”Concurrency in C# Cookbook” (O’Reilly Media, 2014). Hänen kotisivunsa ja bloginsa on osoitteessa stephencleary.com.
Keskustele tästä artikkelista MSDN Magazinen foorumilla
.
Vastaa