bhawk.hu – Magyar fejlesztői blog

bhawk.hu – Magyar fejlesztői blog


2019. augusztus
h k s c p s v
« máj    
 1234
567891011
12131415161718
19202122232425
262728293031  

Kategória


Async, await: Aszinkron hívások egyszerűen

Gulyás GáborGulyás Gábor

A Visual Studio 2012-es verziójával egy teljesen új opciót kaptunk az aszinkron hívások megírására és kezelésére. Sokak számára, főleg az új fejlesztőknek jelent nagy problémákat az új koncepció megértése, pedig egy végtelenül egyszerű megoldásról van szó, mely kódunkban hatalmas előnyöket jelent strukturálisan és logikailag egyaránt.

A szoftverfejlesztésben már nagyon régre nyúlik vissza a többszálú működés gyakorlata. Sokak számára valószínűleg nem kell ennek az előnyeit bemutatnom, de egy nagyon egyszerű példát mégis felhoznék, amivel még első alkalmazásom megírásakor találkoztam. Egy mobil alkalmazást készítünk, melynek a felületén egy lista jelenik meg, fájlok tartalmával. A tartalmukat átírtuk és szeretnénk ezeket az átírt elemeket visszamenteni a fájlrendszerbe. Vajon mi történik, ha ezt ugyanazon a szálon tesszük ahol a felület fut?

Az egész alkalmazás megakad, a felület nem reagál, mintha lefagyott volna a program. Ez azonban nem így van, a háttérben azon dolgozik az alkalmazásunk, hogy mindent visszamentsen és amint ez megtörtént, a felület újra életre kel. Így nem szeretnénk kiadni alkalmazásunkat igaz? Erre – is – jelent megoldást a többszálú működés, mellyel elkülönítve hajthatunk végre egy erőforrás-igényes feladatot, így az nem fogja megakasztani a felületet.

Az async, await kulcsszavak előtt

Mielőtt még belemennénk ennek az új megoldásnak a rejtelmeibe, ismerkedjünk meg azokkal a lehetőségekkel, amik régen álltak rendelkezésünkre egy ilyen probléma megoldására.

AsyncExample

Az alábbi osztályokhoz mind-mind egy közös példaprogramot hoztam létre, mely mindből példányosít egyet-egyet, hogy megmutassa, miként is működnek azok. Javaslom, hogy a kódot töltsd le és tekintsd át, mert nem minden részére fogok a cikkben kitérni.

Thread

Az egyik lehetőségünk a Thread osztály. Nevéből adódóan ezt az osztályt arra találták ki, hogy egy új szál létrehozásával a hosszan futó, erőforrás-igényes folyamatokat a felület megakasztása nélkül hajthassunk végre. A Thread erőssége abban rejlik, hogy roppant egyszerűen deklarálható, nem túl körülményes, a szálakat egymásba láncolhatjuk egy ThreadPool segítségével – ebben a cikkben erre nem fogok kitérni most -, futás közben akár szüneteltethetjük őket, folytathatjuk, stb. Használata akkor javasolt, ha a háttérben olyan folyamatokat akarunk végrehajtani, melyek a felhasználói felülettel nem állnak kapcsolatban, nem jelenítenek meg új elemeket a felületen. Ilyen lehet például a fájlok másolása, egy tömörítés, I/O műveletek, stb.

A Thread-hez kapcsolódó kódpéldát a következő GitHub címen találhatod meg: https://github.com/Bhawk90/SampleCodes/blob/master/AsyncExamples/AsyncExamples/ThreadExample.cs

A Thread használata nem javasolt, ha a felületen szeretnénk valamilyen módon követni a munkafolyamatot, azonban ez nem jelenti azt, hogy lehetetlen. Mivel most egy másik szálon vagyunk, nem azon amelyikről a felület fut, ha hozzá szeretnénk férni a felület valamely eleméhez, rögtön egy hibát kapunk, mert egy másik szálhoz kapcsolódó objektumhoz szeretnénk hozzáférni. Erre megoldást jelent a következő kód:

private void WriteToOutput(string txt)
{
    // Since we are on a different Thread then the UI, we must use a Dispatcher to communicate with it.
    // Without using a Dispatcher to the UI Thread, we would receive a cross-thread violation.
    outputControl.Dispatcher.BeginInvoke((Action)(() =>
    {
        outputControl.Text = txt;
    }));
}

Az outputControl egy felületi elem, jelen esetben egy TextBlock, amire kiírnánk egy szöveget. A Dispatcher.BeginInvoke az adott felületi elemhez tartozó szálhoz kér egy referenciát és futtatja le rajta az első paraméterként megadott függvényt.

BackgroundWorker

Második körben ismerkedjünk meg egy olyan osztállyal, ami a Thread-re épül: BackgroundWorker.

A BackgroundWorker kissé körülményesebben működik, de megoldást kínál arra, hogy futása közben jelzést adhassunk a felület számára, hogy éppen hol, hány százaléknál tart a folyamatunk, annak végeztével pedig tetszőleges adatokat jeleníthetünk meg, mondjuk a feldolgozott fájlok listáját.

Tekintsünk meg egy egyszerű példakódot egy BackgroundWorker-re (GitHub):

public BackgroundWorkerExample()
{
    bgWorker = new BackgroundWorker();
    bgWorker.WorkerReportsProgress = true;
    bgWorker.DoWork += BgWorker_DoWork;
    bgWorker.ProgressChanged += BgWorker_ProgressChanged;
    bgWorker.RunWorkerCompleted += BgWorker_RunWorkerCompleted;
}

private void BgWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // Resource intensive task running
    for (int i = 0; i < 4; ++i)
    {
        bgWorker.ReportProgress(i * 25);
        Thread.Sleep(1000);
    }
}

private void BgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    outputControl.Text = "BackgroundWorker Progress: " + e.ProgressPercentage;
}

private void BgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    outputControl.Text = "BackgroundWorker Finished";
}

A példányosítást követően a DoWork, valamint a RunWorkerCompleted eseményekre mindenképpen kössünk valamilyen metódust. Előbbi lesz maga a feladat ami futni fog az újonnan létrehozott szálon, utóbbival pedig tetszőleges hívásokat köthetünk a folyamat végéhez.

Megjegyzés: Egyedül a DoWork fut különálló szálon, így a többi eseményben teljes hozzáférést kapunk a felületi elemekhez, ettől nem kell tartanunk.

A ReportProgress hívással, valamint a ProgressChanged eseménnyel követni tudjuk a felületen, hogy éppen hol tart a folyamat, azonban ne felejtsük el az ehhez tartozó tulajdonságot igaz értékre állítani a BackgroundWorker-en (WorkerReportsProgress). Enélkül a programunk hibával el fog szállni.

A BackgroundWorker használatát nem javaslom abban az esetben, ha össze szeretnénk láncolni több különálló, háttérben futó feladatot. Képes rá ez a megoldás is, ha a RunWorkerCompleted-ből hívjuk meg a következő BackgroundWorker példányt, de ez nagyon csúnya kódot eredményez és nehézkes lenne a karbantartása.

Async/await Task

A Task osztály és a hozzá kapcsolódó async és await kulcssavak egy új koncepciót vezettek be az aszinkron hívások terén, amit már rengeteg különböző függvénykönyvtárban is megtalálhatunk, ilyen például a HttpClient is. A fejlesztőknél a legtöbbször az jelentette a problémát, hogy egy aszinkron hívás lefutása után valahogy meg kellett oldani, hogy a függvényből kapott eredménnyel valamilyen módon tovább kellett dolgozni. Ezt elegáns módon nem volt egyszerű megoldani, ugyanis egy BackgroundWorker esetében ezt mindig a RunWorkerCompleted-ben kellett megtenni. Ha pedig ide is egy BackgroundWorker került, ami újabb aszinkron munkát végzett, akkor a végén már átláthatatlanná vált a kód.

ComplexBgWorker

 

Az async és await kulcsszavakkal sokkal egyszerűbbé vált aszinkron függvények megírása, gyakorlatilag minimális különbség van a kettő között. Tekintsünk meg egy egyszerű kódpéldát:

public async Task<string> GetStringContentAsync(string uri)
{
    var client = new HttpClient();
    var content = await client.GetStringAsync(uri);

    return content;
}

A függvény egy új HttpClient objektumot példányosít, melynek segítségével lekérdezi egy adott weblap tartalmát, majd visszatér azzal. Lássunk néhány fontos tudnivalót az async függvényekkel kapcsolatosan:

Az előzőekkel szemben az async és await kulcsszavak használatával nem hozunk létre új szálakat, azonban nem akasztják meg a program futását. Tehát amennyiben írunk egy új async metódust, melyben egy Thread.Sleep(1000) parancsot hívunk meg, a felületünk látszólag meg fog fagyni. Ilyen esetekben a Task.Run parancsot kell használni, a következő módon:

await Task.Run(() => {
    Thread.Sleep(1000);
});

Ekkor a Task egy új szálon futtatja le a Thread.Sleep(1000) hívást, eredeti async hívásunk pedig amiben ezt megírtuk, az await kulcsszó miatt ezt az egy másodpercet meg is fogja várni, anélkül, hogy a felület megfagyna.

Ha pedig egy kicsit komolyabb példát szeretnénk látni egy async hívásra, akkor tekintsük át a következő néhány sort:

public async Task<string> DoSomeWorkAsync() {
    var uri = await Task.Run<string>(async () => {
        await Task.Delay(1000);
        return "http://bing.com/";
    });

    return await GetStringContentAsync(uri);
}

Az async/await kulcsszavakhoz megírt kódpélda szintén elérhető a GitHub oldalamon.

Végszó

A cikket egy betekintőnek szántam az aszinkron hívások és függvények világába, hogy lássátok milyen lehetőségekkel álltok szemben. Többet szeretnétek tudni? Valami nem teljesen világos? Szóljatok hozzá a cikkhez, tegyétek fel kérdéseiteket!

Minden ami Microsoft technológia! Több mint 8 éve foglalkozom programozással, ez idő alatt pedig rengeteg nyelvet elsajátítottam, leginkább a C#-ot kedvelem! Jelenleg szoftverfejlesztőként dolgozom!

Comments 3
  • Iván
    Posted on

    Iván Iván

    Válasz Author

    Jó kis cikk, köszi szépen!


  • Eszter
    Posted on

    Eszter Eszter

    Válasz Author

    Nagyon szépen köszönöm a cikket! Sok mindent helyrerakott 🙂


  • Gábor
    Posted on

    Gábor Gábor

    Válasz Author

    Érthető, sallang mentes cikk.
    Nagyon jól írsz! Csak így tovább.
    Köszönöm!


This site uses Akismet to reduce spam. Learn how your comment data is processed.