C# 5.0 aszinkronitás röviden

Ahogy a cím is mutatja, a C# 5.0-ban megjelent nyelvi szintű aszinkronitást mutatom be – viszonylag röviden, gyakorlati szemszögből.

Ebben a bejegyzésben azzal próbálkozom meg, hogy egyszerűen és közérthetően írjam le a .NET 4.5-ben megjelent új nyelvi lehetőség használatát.

Nézegettem videókat, olvastam róla cikkeket, nomeg – a lényeg – használtam sokat, és azt látom, hogy gyakran meglehetősen bonyolultan magyarázzák. Pedig a C# nyelvi szintű aszinkronitásának éppen az a szépsége, hogy annyira egyszerű, hogy könnyű megérteni. Persze, ez csak az igazság fele…

A teljes igazság az, hogy mint minden compiler feature-nél (fordító képesség? Mindenképpen le kell ezt fordítanom? 🙂 ), itt is átvernek minket, de persze csak a mi érdekünkben. Magyarul a tényleges működés annál jóval bonyolultabb, mint ami a felszínen látszik, de ez nem kell, hogy zavarjon minket, ha helyesen tudjuk használni.

Mi is ez az aszinkronitás?

Az aszinkronitás előzményeinek fejtegetésébe nem mennék bele, elég, ha a következőt tudjuk: a nyelvi szintű aszinkronitás célja, hogy olyan metódusokat írhassunk, melyek nem blokkolják a hívó szálat, de emellett nem is kell miattuk részekre bontani a hívó metódus üzleti logikáját.

Vegyünk egy WPF alkalmazást, ahol egy gombra való kattintás valamilyen műveletet indít. Tehát adott egy  eseménykezelő metódusunk – tartalmazzon 3 lépést:

private void Start_Click(object sender, RoutedEventArgs e)
{
    tbkStatus.Text = "Elindítva";
    Method1();
    int result = Method2();
    tbkStatus.Text = "Vége";
    tbkResult.Text = result.ToString();
}

A tbkStatus és a tbkResult TextBlockok. Alább látható a Method1 és Method2 metódus.

private void Method1()
{
    Thread.Sleep(2000);
}

private int Method2()
{
    Thread.Sleep(2000);
    return new Random().Next();
}

Ahogy látható, tulajdonképpen semmilyen értelmes munkát nem végeznek, csak az a céljuk, hogy szimuláljanak egy-egy összetett feladatot. A második metódus egy véletlen egészszámot ad vissza. Néhány fontos dolog azonban van:

  • A Method2 csak az után futhat, hogy a Method1 lefutott. (Ennek itt most nem lenne jelentőssége, de feltételezzük, hogy a Method1 lefutása hozza megfelelő állapotba a programot ahhoz, hogy a Method2 futhasson.)
  • A “Vége” felirat csak a Method2 lefutása után jelenhet meg a felületen.
  • A tbkResult szövegének beállításához szükséges a Method2 eredménye.

A futtatjuk a programot, és meghívjuk a StartButton_Click metódust, akkor azt vehetjük észre, hogy az “Elindítva” szöveg nem jelenik meg a tbkStatusban. A UI kb. 4 másodpercre válaszképtelenné válik, majd egyszerre jelenik meg az eredmény és a státusz, ami arról tájékoztat minket, hogy visszatért a metódus.

Ez a UI-lefagyás egy nem kívánt mellékhatása annak a ténynek, hogy a metódust szinkron módon futtatjuk. Vagyis a UI-t futtató szálnak kell 4 másodpercet várnia, és ezalatt nem foglalkozhat azzal, hogy a felhasználó éppen mit ügyködik a felületen. Hogyan kerüljük ezt el? (Mármint nem azt, hogy a felhasználó ügyködjön a felületen…)

A .NET rengeteg módszert ad erre. Körülbelül hármat. 🙂

Először is használhatunk közvetlenül szálakat. Másodszor használhatjuk valamelyik .NET 1.0 óta jelenlévő aszinkron programozási mintát – amik a háttérben egyébként jellemzően megint csak szálakra vezethetők vissza. Harmadszor használhatjuk a .NET 4-ben megjenelt Task Parallel Library-t. (A BackgroundWorkertől és hasonló specializált lehetőségektől most az egyszerűség kedvéért eltekintek.)

A Task Parallel Library-t nem a “puszta aszinkronitás” céljából hozták létre, hanem azért, hogy egyszerűen használhassuk ki a több processzormag előnyeit, ha éppen valamilyen párhuzamosítható algoritmust írunk. Ettől függetlenül megjelent egy Taskokra (a TPL egyik alap építőköve) épülő aszinkron programozási minta a korábbiak mellett. Ennek lényege, hogy a valószínűleg hosszú lefutású műveleteket egy-egy Taskba csomagoljuk, ami automatikusan egy másik szálon futtatja le a műveletet. (Illetve lehet, hogy ugyanazon a szálon – erről majd egy másik cikkben, amikor a TPL lesz terítéken.) De a lényeg: ezeket a Taskokat össze lehet fűzni úgy, hogy az egyik kimenetét a következő bemenetévé tesszük, és a “folytatás” automatikusan elindul, amikor az “előzmény” eredménye megszületett. Nagyon szép és erős módszer arra, hogy aszinkron módon futtassunk egy többlépéses, hosszú lefutású folyamatot.

De vajon szép-e kódban is? A fenti három metódus így néz ki, ha Taskokra építjük őket:

private Task Method1Task()
{
    return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(2000);
        });
}

private Task<int> Method2Task()
{
    return Task.Factory.StartNew<int>(() =>
        {
            Thread.Sleep(2000);
            return new Random().Next();
        });
}

private void StartTask_Click(object sender, RoutedEventArgs e)
{
    tbkStatus.Text = "Elindítva";
    Method1Task()
        .ContinueWith<int>(prev => Method2Task().Result)
        .ContinueWith(prev2 =>
            Dispatcher.Invoke(() => {
                tbkStatus.Text = "Vége";
                tbkResult.Text = prev2.Result.ToString();
            }));
}

Szép? Ha már láttunk APM-et, vagy egyéb módszert, akkor igen, szép, de igen messze van a korábbi (szinkron) kód szépségétől és egyszerűségétől. Vegyük sorra, mi is változott!

  • (A Method1 és Method2 nevének változása nem szükséges, csak a korábbi metódusoktól való megkülönböztethetőség miatt került be.)
  • A Method1 és Method2 belsejében a kód hosszú lefutású részét (ez itt most az egyszerű metódusok miatt a kód egésze, de ez nem szükségszerű) egy-egy Taskba csomagoltuk, és a metódusok ezt a Taskot adják vissza. A Method2 olyan Taskot, amelynek eredménye egy int.
  • A Method1Task már nem void, helyette Task a visszatérési típusa. Erre azért van szükség, hogy bevárható legyen, amíg elvégzi a feladatát. Ha szimplán elindítottuk volna a Taskot, és a metódus voidként visszatért volna, a Method2 anélkül indulhatna el, hogy a Method1 feladata ténylegesen befejeződött volna – hiszen az egy másik szálon fut.
  • A Method2 visszatérési típusa intről Task<int>-re változott, így ez a metódus is azonnal visszatérhet, miközben a háttérben még fut a feladat; de ha valaki meg akarja várni, amíg befejeződik, a kapott Task segítségével képes rá.
  • Az eseménykezelő változott a legtöbbet, hiszen itt kell leírnunk az egész folyamatot – és ezúttal már ügyelve arra, hogy ne blokkoljon, de a helyes időrendben fussanak le a lépések. A metódus belsejét felbontottuk három részre, hogy külön tudjuk választani az egyes lépéseket.
    • Az első lépés… (a begyűjtés…*) Tehát az első lépés, hogy elvégezzük, ami biztos, hogy nem tart sok ideig. Ez jelen esetben az “Elindítva” szöveg kiírása a tbkStatusba.
    • A második lépés, hogy elindítjuk a Method1Taskot.
    • A Method1Task már Taskban futtatja a kódját, vagyis a metódus körülbelül azonnal visszatér, de valójában még nem végzett a feladatával. Ebből kifolyólag a 3. lépést, a Method2(Task) meghívását nem írhatjuk le egyszerűen a Maethod1Task hívása után. Csak akkor indulhat, ha Method1Task eredméne már megszületett, ezért a Method1Task által visszaadott Taskra a ContinueWith metódussal folytatásként írunk fel egy Taskot, ami a Method2Taskot futtatja, és visszaadja annak eredményét.
    • A következő lépés, hogy kiírjuk a képernyőre a “Vége” feliratot, illetve a Method2Task eredményét. Ezzel azonban az a gond, hogy mindenképpen háttérszálon fog futni. Háttérszálról a UI-ra nem tudunk közvetlenül dolgozni, ezért kell a Dispatcher osztály Invoke metódusát használva futtatnunk a kódot.

(Hangsúlyoznám, hogy a metódus ilyen összerakása egy megoldás a sok közül. Lehet máshogy is, embere válogatja, hogy ki milyen struktúrával szereti összerakni az aszinkron kódot. 🙂 )

Mit nyertünk? Ha elindítjuk az eseménykezelő metódust, végre megjelenik az “Elindítva” felirat, a UI nem fagy meg, és kb 4 mp múlva, amikor megjelenik az eredmény, akkor jelenik meg a “Vége” felirat is. Tehát azzal, hogy aszinkronná tettük a kódot, levettük a terhet a UI-szál válláról – cserébe bonyolódott valamelyest a kódunk, hiszen olyan metódusokkal kell dolgoznunk, melyek azt “hazudják”, hogy végeztek, de valójában még futnak a háttérben.

…és akkor végre tényleg C#5.0

Mire is jó nekünk a C# 5.0-ban megjelent új, nyelvi szintű aszinkronitás? Arra, hogy ne kelljen Taskokat egymásra láncolnunk és ne kelljen minduntalan a Dispatchert hivogatnunk. Arra szolgál, hogy a szinkron kód egyszerűségét és átláthatóságát megtartsuk, de közben megkapjuk az aszinkron kód előnyeit is.

A C# 5.0-ban megjelenik egy módosító, az async, illetve egy operátor, az await. Előbbit metódusokon alkalmazhatjuk, utóbbival pedig metódushívásokat jelölhetünk meg. Az async módosítóval ellátott metódusok vagy void visszatérési “típusúak” (vagyis nem adnak vissza semmit), vagy pedig egy Taskot adnak vissza. Amennyiben a metódusunknak ténylegesen vissza kell adnia egy értéket, akkor pedig továbbra is a Task<TResult> típus alkalmazható.

Hogy fest az eseménykezelőnk, ha felhasználjuk a C# 5.0 adta előnyöket? Íme:

private async void StartAsync_Click(object sender, RoutedEventArgs e)
{
    tbkStatus.Text = "Elindítva";
    await Method1Task();
    int result = await Method2Task();
    tbkStatus.Text = "Vége";
    tbkResult.Text = result.ToString();
}

Mi változott? Ne a Taskokra építű kóddal vessük össze a metódust, hanem az eredeti, teljesen szinkronnal!

Az eseménykezelőt megjelöltük az async módosítóval. Ez azért szükséges, hogy használhassuk benne az await módosítót. A kódban azokon a helyeken, ahol egy Taskot visszaadó metódussal van dolgunk, alkalmaztuk az await módosítót.

Ennyi. A Method1Taskon és a Method2Taskon tulajdonképpen nem kell változtatunk, az a lényeg, hogy visszaadjanak egy-egy Taskot. Ha több lépés is lenne bennük, és emiatt használni akarnánk a metódusokon belül is az await operátort, akkor ezeket is megjelölhetnénk az async módosítóval – de ahogy fentebb látszik, ez nem is szükséges az esetünkben.

Látható, hogy

  • Nincs szükség a Taskok egymásra láncolására a ContinueWith vagy egyéb struktúra segítségével, egyszerűen szekvenciálisan leírjuk az utasítások sorozatát.
  • A Task<int>-et visszaadó metódus visszatérési típusa látszólag int – vagyis olyan, mintha a szinkron Method2-t hívtuk volna!
  • Az eseménykezelő legvégén lévő két UI-utasításnál nincs szükség Dispatcherre.

Gyakorlatilag szinkronnak néz ki a kód. Ha futtatjuk, akkor viszont látható lesz, hogy aszinkron módon fut, nem a UI-szálon, illetve természetesen a szövegek is a megfelelő időben jelennek meg a felületen.

Az async és await kulcsszavak segítségével a fordítóra hárítjuk a feladatot, hogy tegye aszinkronná a metódushívásokat. Fordításkor egy állapotgép készül, amely majd kezeli az egyes lépések közötti váltást. Emellett pedig folyamatosan ügyel arra is, hogy az egyes utasítások a megfelelő szálkontextusban futhassanak le: vagyis a metódus végén lévő két UI-műveletről tudja, hogy azon a szálon kell futniuk, amelyről eredetileg meghívták az őket tartalmazó metódust. Ez pedig jelen esetben a UI-szál lesz, hiszen az hívja meg az eseménykezelőnket.

A gyakorlati felhasználáshoz elég ennyit tudnunk. Remélem, elég rövid, de azért érthető is volt. 🙂

*: Az első lépés a begyűjtés. A második lépés a… a harmadik lépés a profit. Bővebb információ az alábbi linken: http://www.youtube.com/watch?v=tO5sxLapAts

 

Update: a bejegyzés tulajdonképpeni folytatása a W8 for It… nevű Windows 8 fejlesztői blogon jelent meg.

Advertisements

~ Szerző: Fülöp Dávid - 2012. szeptember 30..

4 hozzászólás to “C# 5.0 aszinkronitás röviden”

  1. […] –          Fülöp Dávid blogja – C# 5 aszinkronitás röviden -https://chevenix.wordpress.com/2012/09/30/c-5-0-aszinkronitas-roviden/ […]

  2. […] Nem adtunk értéket a paramétereknek, tehát üres stringeknek és egy 0-nak kellene megjelennie, de mégsem. Varázslat? Igen, de ezt a D&D-n kívül “compiler feature”-nek nevezik. (És vannak ennél jóval nagyobbak is, lásd az előző cikket.) […]

  3. […] rég írtam az aszinkronitásról a .NET 4.5 esetében. Most a Windows Store alkalmazások, vagyis a Windows […]

  4. […] Itt elérhető egy korábbi cikkem, amiben bővebben, de azért tömören kifejtem az async/await lényegét. […]

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

 
%d blogger ezt kedveli: