Categories
Programozás

Többszálú programozás Swiftben

Szóval most éppen az iOS, és macOS programozásra adtam a fejem, és egy többszálon futó appon dolgozom. Nem foglalkoztam egy ideje a bloggal (Miért? azé’.. !), de most megpróbálok néhányszor új tartalmat feltenni, mégpedig azért, hogy ha tanulok valami újat, az meg legyen itt, na meg a magyar szakirodalom elég szegényes.

Szóval többszálú, vagyis aszinkron programozásról akkor beszélünk, ha egy programkód részei különböző szálakon (thread-ekben) egyszerre futnak egymás mellett. A szokásos kódok mindig szinkronban, egymás után  futnak le:

func1()
func2() // func2() először akkor lesz meghívva, ha func1() visszatér.

Az alkalmazásokban  a kód szinkron végrehajtása blokkolja a felhasználói felületet. Ha a metódusok néhány ms alatt lefutnak,  az alig észrevehető és nem is jelent problémát, de a hosszabb ideig tartó számításokat, fel – és letöltéseket, fájlműveleteket tartalmazó metódusok alkalmazásakor ideje megbarátkozni a párhuzamos kódvégrehajtással.

A sok időt igénylő számítások felosztása több thread-re több magos processzorok esetén meg tud spórolni egy kis időt. Ez különösen igaz például macOS-en, ahol a nagyteljesítményű mac-ek több maggal rendelkeznek.

A swift önmagában nem tartalmaz nyelvi elemeket a párhuzamos feldolgozáshoz. Lehetett már hallani, hogy ez később implementálva lesz, de ez a Swift 4-ből kimaradt. Talán majd a következő verzióban.. Addig is ennek a problémának a megoldására ott a Dispatch-Framework (GCD), ami minden olyan programban használható, ami a Foundation, Cocoa, vagy UIKit Framework-öt használja.

Az aszinkron programoknál mindig figyelni kell arra, hogy egy objektumot ne változtathasson meg két thread egyszerre. Hogy ezt megelőzzük, a szálakat össze kell hangolni, máskülönben olyan nem várt hibákba futhatunk, aminek a kibogozása rengeteg idő lesz, mivel ezek a hibák általában nehezen reprodukálhatók. A továbbiakban két fontos osztályt nézünk meg közelebbről:
DispatchQueue, DispatchGroup.

DispatchQueues


A DispatchQueues- k feladatok fogadására, és futtatására szolgálnak. A GCD-ben két különböző queue típus van, a soros és a párhuzamos. A soros Queue a feladatokat sorban egymás után dolgozza fel, tehát soha kettőt, vagy többet egyszerre. Ezzel szemben Concurrent-Queues- k a feladatokat párhuzamosan hajtja végre. Itt az először hozzáadott feladatok fognak előbb elindulni, és mivel a feladatok egymással párhuzamosan fognak futni, előre nem is lehet majd meghatározni melyik feladat fog először végezni. Egy alkalmazás indításakor alapból több Queue jön létre.

A Main Queue: Ez egy soros queue, ami az alkalmazásokban a felhasználói felületért és az egész kódért felelős, ami a felhasználói interakciókat kezeli. Minden felhasználói felület változást (pl egy folyamatjelző-sáv haladását) a Main-queue feladatain keresztül kell elvégezni.
Globale Queues (Concurrent Queues): Minden alkalmazásban négy ilyen áll rendelkezésre, méghozzá mind a négy Quality-of-Service-höz egy – egy. A Global azt jelenti, hogy minden kódrész hozzá férhet.

A Main Queue-ra egyszerűen így hivatkozunk: DispatchQueue.main
Global Queue esetén pedig a global metódussal, aminek a paramétere a kívánt Quality-of-Service lesz. Ez egy opcionális paraméter, tehát ha nem adunk meg paramétert, akkor a globális Default-Queue-re fog hivatkozni.

let mainqueue = DispatchQueue.main
let globalQueueHigh = DispatchQueue.global(qos: .userInitiated)
let globalQueueDefault = DispatchQueue.global()"

Szükség esetén saját queue-ket is beállíthatunk magunknak. A gyakorlatban például ez úgy nézhet ki, hogy különböző típusú feladatokhoz hozunk létre egy Queue-t, például egyet a hálózati feadatoknak, másikat adatbáziskezeléshez, egy megint másikat például fájlkonverzióhoz. A DispatchQueue Init-függvényének több paraméter is megadható, de ami minenképp szükséges, az a label: paraméter. Ezzel a paraméterrel a Queue-nak egy rövid és egyértelmű nevet adunk, ami a későbbi hibakeresés során hasznos lehet. // soros Queue, Default-QoS

let q1 = DispatchQueue(label: "ml.chaos-dev.network")

// soros Queue, QoS = .userInitiated
 let q2 = DispatchQueue(label: "ml.chaos-dev.database",
 qos: .userInitiated)

// Concurrent Queue, QoS = .background
 let q3 = DispatchQueue(label: "ml.chaos-dev.conversion",
                                        qos: .background,
                                 attributes: .concurrent)

Most, hogy már felemlegettük párszor a Quality of Service-t, néhány szót erről is ejtenünk kell. A globális queue-k hozzáférésekor, valamint az async metódus használatakor az opcionális qos paraméterrel adható meg, hogy milyen sürgős az adott feladat. Ehhez a következő Dispatchqos.QoSClass enumerációk állnak rendelkezésre:

.userInteractive: A legmagasabb prioritás. Olyan feladatoknak szánták, mint például a felhasználói felület aktualizálása.

.userInitiated: A felhasználó által közvetlenül kiváltott akciók, amiker gyorsan el kell intézni

.utility: Hosszú számításoknak, stb, amiknél általában egy folyamatjelző sávot is alkalmaznak. Az operációsrendszer ezeket a fajta feladatokat lehetőleg energiatakarékosan próbálja elvégezni.

.background: Olyan feladatoknak szánták, amik gyakran a felhasználó tudta nélkül futnak, nem időkritikusak, mint például egy folyamatos állapotfrissítés.

ezekenkívül még a .default és a .unspecified van még definiálva.

A feladatok csoportosítása.

Ha egy feladatot több aszinkron részre osztunk, az eredményt csak akkor lehet feldolgozni ha már minden érintett részfeladat végzett. Ezt a legegyszerűbben úgy lehet felismerni, ha a részfeladatokat egy DispatchGroup -ba csomagoljuk. Ez a notify metóduson keresztül megadja a lehetőséget arra hogy egy Closure adjunk meg, ami akkor fut le, ha a csoport összes részfeladata készen van. A notify egy paraméterben megkapja, melyik DispatchQueue -ben fusson le a Closure. Ha az eredmény például a felhasználói felületet frissíti, akkor a Main Queue kézen fekvő számunkra.

// csoport létrehozása
let mygroup = DispatchGroup()

// több feladat elindítása a csoportban
myq1.async(group: mygroup) { self.prepareStuff() }
myq2.async(group: mygroup) { self.makeMoreThing() }
myq3.async(group: mygroup) { self.createAnotherStuff() }

// done() függvényt a Main Queue-ben végrehajtani,
// ha a feladatok készen vannak
mygroup.notify(queue: DispatchQueue.main) {
    self.done()
}

Néhány esetben szükség lehet arra, hogy egy Closure aszinkron feldolgozása ne azonnal kezdődjön, hanem csak egy meghatározott idő után. Ezt úgy lehet elérni, hogy a Closure-t async helyett asyncAfter -el adjuk át a Queue-nak. Ehhez kell még egy paraméter, ami egy DispatchTime elem fomájában megadja a kód futtatásának időpontját. A jelenlegi időt a DispatchTime.now() segítségével határozzuk meg, amihez még hozzá adható egy double érték is ami másodpercben értendő.

// Closure-t három másodperccel később futtatni.
myqueue.asyncAfter(DispatchTime.now() + 3) { 
    self.doSomething()
}

Lehetőségünk van egy metódust periodikusan is lefuttatni. Ezt megtehetjük egész egyszerűen a Foundation könyvtár Timer osztályának segítségével. Az osztály dokumentációja mindenesetre figyelmeztet arra, hogy nem garantálja a metódushívás idejének pontosságát. A lenti példában egy másodperc alatt nyolcszor kerül meghívásra a ViewController osztály doSomething() metódusa.

let mytimer = Timer.scheduledTimer(
 timeInterval: 0.125,
 target: self,
 selector: #selector(ViewController.doSomething),
 userInfo: nil,
 repeats: true)

// Hívások leállítása
mytimer.invalidate()

A feladatok futtatásával kapcsolatban még fontos azt megjegyezni, hogy ha elindítottunk egy aszinkron feladatot, nincs lehetőség arra, hogy megállítsuk azt, ezért ilyen célból használhatunk egy külső bool típusú változót amit folyamatosan pollozunk, hogy a feladat futhat e tovább, avagy sem.
Például egy mégsem gomb megnyomására beállítjuk a változót false értékre.

A DispatchQueue osztály biztosít számunkra egy suspend metódust, aminek a hatására további feladatok már nem lesznek indítva, de a jelenleg még folyamatban lévő feladatok még lefutnak. A resume metódussal a még fennmaradó feladatok folytathatók.

Dióhéjban ennyi lenne  a párhuzamos programozás alapjai. Köszönöm a figyelmet és remélem, hogy segíthettem ezzel a kis írással.

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

Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöljük.