Okosan ütemezett aszinkron végrehajtás? Ez is UX!
Régen roppant egyszerű volt az autós felhasználói felület: a jármű és az utasok/sofőr közötti kommunikációt a kormány, a pedálok és a mutatók biztosították, a UX ebben ki is merült. Az elektronika azonban alapjaiban felforgatta a kétirányú kapcsolatot - lássuk, hogyan is?
A mai autóknál nem ritka, hogy egyszerre több kijelzőn is fut valamilyen "tartalom", e kijelzők meghajtását pedig többnyire nem egyetlen eszköz végzi, a számítási feladatok szétoszlanak több fedélzeti számítógép között. Vannak közöttük hagyományos egy gép - egy kijelző megoldások, de egyre inkább előretör egy új paradigma, amelyben egyetlen erősebb központi egység egyszerre nem csak több kijelzőt, de több operációs rendszert is képes meghajtani. Utóbbira azért van szükség, mert a feladatok kritikus szintje különbözik, ezeket pedig egyik-másik iparági specifikus operációs rendszer jobban ki tudja szolgálni.
Fürtözött autós rendszerek
Tipikus példa a műszerfal-kijelző kirajzolása: ezt a megbízhatóbbnak és biztonságosabbnak számító QNX- vagy Linux-alapú rendszer végzi, mivel az itt megjelenő információk a vezetés szempontjából kritikusak. A középkonzol médiacentere viszont nem számít ennyire fontosnak, így annak felületét sokszor Android alól vezérlik. És akkor még nem is beszéltünk arról, hogy a felhasználók sokszor saját eszközeiket, telefonjukat, tabletjüket is behozzák az autóba és azt össze is szeretnék kötni a gépkocsi rendszerével - ezt is kezelni kell valahogy.
A fentiek miatt az autós UX, a klasszikus értelemben vett felhasználói élmény kérdése egészen különleges feltételeket támaszt a fejlesztők számára. Például az autó nem indulhat lassan: néhány másodperccel a kulcs elfordítása után már mindennek futnia-működnie kell, ezt az igényt a szoftvermérnököknek megfelelő trükkökkel és megoldásokkal kezelniük kell. Ami szintén a terület jellemzője: az autóipar szélsőségesen fragmentált szoftveres szempontból. Ahogy láttuk egyetlen autón belül is több operációs rendszer dolgozik együtt, más-más gyártók pedig egészen más mixekre esküsznek. E platformokat egy autóipari beszállítónak saját hatáskörben kezelnie kell, ráadásul költséghatékonyan.
Az NNG megoldása: kitalálni egy közös C++ magot, amelyre a szoftverfejlesztések épülnek és ezt minimális platformspecifikus "ragasztóanyaggal" szállítani a különböző gyártók egyedi rendszereire. Fontos elvárás a skálázhatóság és az elosztott működés is: a leszállított szoftver egyes elemei ugyanis az egyik OS-en, más modulok egy másik OS-en, míg bizonyos részeik például az utas telefonján fognak futni. Így például a hátsó ülésen ülő utas a tabletjén kezelheti a navigációs rendszer térképét és javasolhat a navigációnak köztes úticélokat (például egy fagyizást).
Alap elvárás persze, hogy még az elosztott rendszer is legyen gyors és gördülékeny, még akkor is, ha bizonyos műveleteket más gépen hajtunk végre. A késleltetés minimalizálásához és az elosztott működéshez vezettük be a legtöbb elem aszinkron futását - ez azt jelenti, hogy az egyes komponensek (ha nem feltétlenül szükséges) nem várnak egymásra. Jelenleg az NNG szoftverében nem minden függvényhívás teljesen aszinkron, de az I/O vagy a feldolgozás, tehát a legproblémásabb elemek már azok. Ez sok problémára megoldást jelentett, hátránya viszont, hogy az aszinkron programozás nehezebb és lassabb, nagyobb terhet ró a fejlesztőre.
Ünnepi mix a bértranszparenciától a kódoló vezetőkig Négy IT karrierrel kapcsolatos, érdekes témát csomagoltunk a karácsonyfa alá.
A jelenség különösen prototipizálásnál erős, amikor maga az architektúra is képlékeny még. Az aszinkron programozáshoz ugyanis nem csak az adott feladatot kell megoldani, de a teljes környezetet ismerni kell a hatékony integrációhoz. Ahogy az egyes programszálak között egyre több függőség van, azzal párhuzamosan nő az aszinkron működésből származó problémaforrások száma is. Néhány egyedi aszinkron működés már komolyabb, szabványosított adatáramlást biztosító technikát kíván meg, ezek száma pedig gyorsan el tud szabadulni. Ráadásul ezek általában egyben készülnek el az aszinkron logikával, így sokkal kevésbé újrahasznosíthatóak e mechanikák és logikák.
Mivel a működés jelentős részét magába az architektúrába kódolja ez a megközelítés, a kulcsot a teljes kódbázis működését ismerő szoftvermérnökök, az architektek képezik - az ő idejük viszont nagyon drága az egyszerű feladatok megoldásához és kevesen is vannak. A megközelítés megemeli a szervezet belépő szakmai szintjét is - elvárás lesz az aszinkron kód írása minden fejlesztővel szemben (legyen az junior vagy szenior).
Többszálúsítás - elemi elvárás autós környezetben
PC-s környezetből is ismert probléma, hogy ha a szoftver minden tevékenységet egyetlen szálon végez, akkor egyes időigényes feladatok (például egy I/O-intenzív betöltés) meg tudják akasztani azt, ami keresztbe tesz a felhasználói élménynek. A programozó feladata ezért az ilyen tevékenységeket külön szálakba rendezni, azok eredményét pedig készen visszaadni a fő programszálnak. Gyakori megoldás, hogy dedikált I/O szál van, amely például beolvassa a textúrát, majd az eredményt egy queue-ban adja vissza. Ez a késleltetésen, szaggatáson enyhít valamelyest, például amíg megérkezik az eredmény, addig továbbra is rajzolhatjuk a műszerek mutatóit. Probléma viszont, hogy e viszonylag egyszerű forgatókönyv nyomán máris két queue mechanizmust kellett építeni és fenntartani, ráadásul ezek újrafelhasználhatósága is kérdéses.
A sokrétű feladat tehát adott volt: válasszuk szét az adott műveletet végző kódot a környezetétől, legyen egy mechanizmus, ami ezeket a feladatokat aszinkron elvégzi, az eredményeket lehessen összefűzni és egymás között átadni, lehessen biztosítani, hogy bizonyos feladatok ne fussanak egyszerre, skálázódjon a kialakított rendszer az rendelkezésre álló erőforrásokkal és persze működjön minden célplatformon.
Házi megoldással
A problémára az NNG házon belül fejlesztett egyedi megoldást, ami a TaskScheduler (vagy simán Scheduler) névre hallgat. Ez lehetővé teszi, hogy az egyes feladatok leprogramozásánál maradjon a szinkron szemlélet, ezeket a Scheduler fűzi láncba és biztosítja, hogy az egyes feladatok eredményei átadódjanak. Emellett a megoldás vezérli a feldolgozó szálakat, priorizálja, időzíti és elosztja a szálak között a feladatokat és gondoskodik az eredmények célba juttatásáról.
Az aszinkron kód írója tehát szinkron részfeladatokat tud írni, majd ezekből a Scheduler állít össze megfelelő feladatokat. Mivel ezt az összefűzést mindenhol a Scheduler végzi, ezért az implementált részegységek újra fel is használhatóak, mivel nincsenek egyedi queue mechanizmushoz kötve. Az alkalmazásban a Scheduler az egyetlen, amely az aszinkron végrehajtást kezeli, más aszinkron mechanika ezen kívül nincs.
Vannak azért dedikált szálak is, bizonyos platformokon bizonyos műveleteket (például kirajzolás) csak bizonyos szálakon végezhetünk. Ilyen esetben segít, hogy a feladatok paraméterezhetőek, így utasítható a Scheduler, hogy ezeket csak az adott dedikált szálon hajtsa végre.
A megközelítés előnye, hogy például a textúrák betöltéséért felelős mechanizmus a kódban jól láthatóan elkülönül és egy közös helyen van. Nem kell a queue-kat és az azokat használó feladatokat keresgélni a projektben, hanem a teljes aszinkron folyamat egyben, világosan áttekinthető. A feladatok felosztása az erőforrások kihasználását is javítja, a sok egyszerűbb, kisebb feladat között a Scheduler jobban el tudja osztani a kapacitást és így hatékonyabban használja ki a platform teljesítményét.
A Scheduler feladata a párhuzamos végrehajtás ütemezése is - nyilván két, ugyanarra a szálra helyezett feladat nem fog egyszerre futni, így az ütemező gondoskodik arról, hogy külön szálra kerüljenek. A másik eszköz egy saját using_resources attribútum, amellyel megjelölhető, hogy a feladat milyen erőforrásokat használ, így a Scheduler gondoskodni tud arról, hogy egyszerre ne fussanak olyan feladatok, amelyeknél a használt erőforrásoknak bármilyen közös metszete van. Így érhető el, hogy következetesen definiált erőforrásokkal lock-mentes kódot írhatunk, a feladatok nem várnak majd egymásra.
Emellett a Scheduler dolga az is, hogy a nem szálbiztos (thread-safe) könyvtárakat kizárólag egy szál hívja egyszerre. Ilyen például a szöveg betű és glyph kiírásához, raszterizálásához használt Freetype könyvtár is - a using_resources biztosítja, hogy két glyph-betöltő ne futhasson egyszerre.
Ha a Scheduler minden feladatba belelát, akkor szinte adja magát, hogy teljesítménymonitorozásra is bevessük. A rendszer futás közben az eseményeket naplózza, ezt tetszés szerint fel lehet dolgozni és vizualizálni a program futását, az egyes feladatok interakcióit, a Scheduler döntéseit, stb. A csavar a dologban, hogy a már említett, több eszközből álló rendszerben is működik, az egyes hardverek logjait együtt is képes kezelni, így a gépek között átívelő műveletekben is kimutatható a fennakadás, lassulás. A fenti képen például egy textúrát kell(ene) egy másik gépről megszerezni, ez azonban elcsúszik, mert azon épp egy hosszú feladat fut.
A megoldás több ponton hasonlít a Microsoft-féle PPL/PPLX könyvtárra, másképp csináljuk viszont a szálak elosztását, így lehetnek dedikált végrehajtó szálaink. És nem mellékesen a Scheduler olyan platfomrokat is támogat, amelyeket a PPLX nem, így fut QNX-en vagy Emscripten segítségével böngészőben is, plusz említhetjük a fenti integrált vizualizáló és teljesítményelemző eszközt is.
A cikk szerzője Perneky László, az NNG UX csapatának egyik vezetője. László 2012-ben csatlakozott az NNG-hez, fő szakterülete a hardware- és platformközeli rendszerek tervezése és fejlesztése. Korábban AAA játékok fejlesztésével foglalkozott.