A felhőalapú technológiákra váltás egy izgalmas kihívás - ha tudod miként kezdj neki! Ebben a cikkben szeretném megosztani...
Async, await: Aszinkron hívások egyszerűen
Gulyás GáborA 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.
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:
[ecko_code_highlight language=”c#”]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;
}));
}[/ecko_code_highlight]
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):
[ecko_code_highlight language=”c#”]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”;
}
[/ecko_code_highlight]
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.
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:
[ecko_code_highlight language=”c#”]public async Task<string> GetStringContentAsync(string uri)
{
var client = new HttpClient();
var content = await client.GetStringAsync(uri);
return content;
}[/ecko_code_highlight]
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:
- Minden ilyen függvény esetében, a visszatérési érték előtt szerepelnie kell az async kulcsszónak – a függvény törzsében csak ekkor használhatjuk az await kulcsszót.
- Konvekció, hogy a függvény neve egy “Async” végződést kap, ezzel is jelezvén, hogy egy aszinkron hívásról van szó
- Visszatérési értéke három különböző értéket vehet fel:
- Task: ebben az esetben nincsen visszatérési értéke a függvénynek,
- Task<T>, ahol T a visszatérési érték típusa,
- Void: ebben az esetben szintén nincs visszatérési érték, csak azért async a hívás, hogy az await kulcsszó használható legyen.
- Egy async hívásban legtöbb esetben szerepel egy await kulcsszóval ellátott hívás is. Egy ilyen híváshoz érve az alkalmazás nem lép tovább egészen addig, amíg az await kulcsszóval megjelölt hívás vissza nem tér.
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:
[ecko_code_highlight language=”c#”]await Task.Run(() => {
Thread.Sleep(1000);
});[/ecko_code_highlight]
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:
[ecko_code_highlight language=”c#”]public async Task<string> DoSomeWorkAsync() {
var uri = await Task.Run<string>(async () => {
await Task.Delay(1000);
return “http://bing.com/”;
});
return await GetStringContentAsync(uri);
}[/ecko_code_highlight]
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!