IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

1. diel - Úvod do viacvláknových aplikácií vo Swift

Vitajte u prvej lekcie Swift kurzu, ktorý vás naučí, ako vo vašich iOS, MacOS či ďalších aplikáciách napísaných v programovacom jazyku Swift využiť maximálny potenciál zariadenia. Súčasné iPhone aj iPad modely disponujú niekoľkými procesorovými jadrami, nemluvňa o strojoch s MacOS. V predvolenom stave ale náš program vždy využije iba jedno jadro. To môže znamenať napríklad štvrtinu dostupného výkonu.

A12 Bionic - Paralelné programovanie a viacvláknové aplikácie vo Swift

Predsa mega výkon A12 Bionic nechcete nechať ležať ladom, nie? :-)

Využitie viac vlákien (multithreading) je ale dôležité z oveľa podstatnejšieho dôvodu, než zapriahnuté všetkých dostupných jadier procesora. Aplikácia majú štandardne jedno hlavné vlákno (označované ako Main alebo UI thread), ktoré skrátka vykonáva náš všetok kód.

Toto hlavné vlákno má za úlohu aplikáciu spustiť, vykresliť užívateľské rozhranie, vykonať naše metódy a veľa ďalšieho. Rovnako tak užívateľské rozhranie následne obsluhuje, takže ak napríklad v iOS aplikácii chceme scrollovať komponentom TableView, na pozadí je to opäť práca pre hlavné vlákno, aby všetko korektne obstaralo.

Čo myslíte, že sa stane s aplikáciou, keď bude toto hlavné vlákno zaneprázdnené čímkoľvek iným? Skúste porozmýšľať ...

Aplikácia sa jednoducho bude tváriť zaseknutá do doby, než hlavné vlákno dokončí predchádzajúcej práci a vráti sa na obsluhu užívateľského rozhrania. Asi nie je potrebné diskutovať nad tým, že túto situáciu v aplikáciách jednoducho mať nechceme. Užívateľom je nepríjemné, keď na ne aplikácie nereaguje :)

Asi najčastejšie sa nám podobná situácia môže stať, keď budeme sťahovať dáta z internetu. Pokojne to pri vývoji ani nemusíme zistiť, pretože sťahujeme pár kilobajtov JSON dát a máme vždy rýchly a spoľahlivý internet. Hlavné vlákno tak dáta stiahne prakticky okamžite k zaseknutiu aplikácie nedôjde.

Lenže v praxi môžeme naraziť na zahltený server, ktorému bude odpoveď trvať napríklad dve sekundy. Alebo zrovna mobil užívateľa bude pripojený k preťažené WiFi či na EDGE mobilné pripojenie a razom máme zaseknutú alebo zasekanie aplikácii (ak sťahujeme viac dát).

Hello GCD!

Vo Swift nie je viacvláknové programovanie tak komplikované, ako môže z názvu znieť. V množstve jazykov musí programátori vytvárať vlákna ako objekty, potom sa starať o ich spustenie, dokončenie a ktovie čo ešte. Vo Swift síce možno triedu Thread vyhrabať, ale prakticky sa nepoužíva. Máme totiž oveľa lepšie riešenie.

Zoznámte sa s GCD, už čoskoro vašim pomocníkom pre krotenie vlákien. Za skratkou sa ukrýva Grand Central Dispatcher a jeho úlohou je spravovať vlákna a zbaviť programátora zložitosťou. Vo väčšine prípadov budeme len GCD úkolovať, že určitý kód chceme vykonať na pozadí (background thread) a nejaký iný zas na hlavnom vlákne. To je celé.

K hlavným vláknu si musíme povedať ešte jednu dôležitú vec. Hovorí sa mu tiež UI thread, pretože má na starosť užívateľské rozhranie. To už vieme. Dôležité ale je, že iba toto hlavné vlákno môže "na užívateľské rozhranie siahať".

Jednoducho nemôžeme meniť vlastnosti UI komponentov, animovať je alebo ovládať z ostatných vlákien. Aplikácia jednoducho spadne. Často vás ale Xcode upozorní práve na tento problém, takže nie je príliš zaludny na odhalenie.

DispatchQueue

Teória už bolo dosť, tak si poďme ukázať skutočný kód pre prácu s GCD. Najzákladnejšie prevedenie kódu na pozadí môže vyzerať takto:

DispatchQueue.global().async {
            // náš kód
}

A to je celé! DispatchQueue slúži na komunikáciu s GCD a pomocou global().async za nás automaticky prevedie akýkoľvek kód vo vlákne na pozadí. Nás vlastne ani nezaujíma, aké vlákno to bude, či si GCD vytvorí nové alebo využije nejaké už vytvorené. Proste vykonáme kód na pozadí, nezablokuje užívateľské rozhranie a nenašteveme užívateľa :-)

DispatchQueue.main

Vyššie spomínaný kód je síce veľmi jednoduchý, v praxi ale veľakrát stačiť nebude. Často totiž chceme nejaké dáta stiahnuť a následne je užívateľovi zobraziť. A ako už vieme, manipulovať s užívateľským rozhraním môže iba hlavné vlákno aplikácie.

To nie je problém, jednoducho opäť využijeme DispatchQueue, avšak tentoraz DispatchQueue.main a potrebné úpravy vykonáme tu. Vyzerať to môže nasledovne:

DispatchQueue.global().async {
            // stažení obrázku
            DispatchQueue.main.async {
                // aktualizace UIImageView
            }
}

Týmto sa "prepneme" späť na hlavnú vlákno a upravíme treba UIImageView, do ktorého načítame novo stiahnuté dáta.

QoS čiže kvalita služby

GCD tiež môžeme povedať, ako je pre nás vykonanie daného kódu dôležité a ten sa podľa toho zariadi. Opäť sa používa metóda global(), ale s parametrom určujúcim práve QoS. Používajú sa štyri voľby v závislosti na tom, čo chceme urobiť:

  • .userInteractive - najvýkonnejší variant, používa sa v prípade interaktívnej práce s aplikáciou.
  • .userInitiated - pre prevedenie akcií, ktoré si vyžiadal používateľ. Napríklad export dát z aplikácie, načítanie ďalších informácií
  • .utility - pre činnosti, ktoré môžu zabrať dlhší čas a nevyžaduje okamžitej prevedení. Často sa používa na sťahovanie / nahrávanie dát.
  • .background - najnižšia priorita, typicky pre činnosti, o ktorých užívateľ nevie. To môže byť indexovanie alebo napr. Vykonávanie zálohy.

Tieto voľby potom použijeme nasledovne:

DispatchQueue.global(qos: .userInitiated).async {

}

Môže byť lákavé vždy použiť najvýkonnejší .userInteractive, ale nerobte to. Ak totiž zrovna užívateľ bude vo vašej aplikácii robiť niečo náročného a vy na pozadí spustíte treba indexovanie s .userInteractive, môžete aplikáciu ľahko výrazne spomaliť.

Pozor na retain cycle

Teraz je potrebné vykonať dlhší odbočku od samotného GCD a prebrať potrebné základy o tzv. Closure capturing. Už vieme, že closure je jednoducho kus kódu, ktorý môžeme uložiť do premennej a tak ho odovzdávať a spúšťať. Tak si môžeme pripraviť činnosti na vykonanie na neskôr, napr. Obsluhu nejakej udalosti atď.

Problém môže nastať, keď naše closure potrebuje k fungovaniu externej premennej, treba celý ViewController, pretože potrebuje zavolať niektorú z jeho metód. Dôjde ku spomínanému closure capturing, čo znamená, že si closure tieto premenné "chytí" a drží si ich, aby ich mohla následne použiť.

Pozrime sa na nasledujúce kód:

var pocitadlo = 1
let closure = {
    let pocitadloPrictene = pocitadlo + 5
    print("Výstup closure: \(pocitadloPrictene)")
}

print(pocitadlo)

pocitadlo += 1
pocitadlo += 1

closure()

print(pocitadlo)

Aký podľa vás bude výsledok? Koľko vypíše print() schovaný v closure ? Ak ste odpovedali, že 6, tak to nie je správne. Správna odpoveď je 8, pretože sa premenná counter pred zavolaním closure zmenila. Môžete si príklad skúsiť v Playground.

Retain cycle

Closure capturing môže veľmi ľahko vytvoriť retain cycle, čo znamená, že dva objekty drží referencie na seba a tým pádom nikdy nedôjde k ich delokácia. Veľký počet takýchto objektov môže potom spôsobiť vyčerpanie pamäte a pád aplikácie. Normálne je objekt dealokován (uvoľnený z pamäte), akonáhle na neho neukazujú žiadne referencie. Respektíve tie silné (čítajte predvolené). Vo Swiftu používame tiež weak referencie, ktoré sa v prípade delokácia nepočítajú. Toto by sme mali poznať a ide len o opakovanie.

Retain cycle sa môže veľmi ľahko stať dosť nepríjemným problémom a treba ho ani nemusíte objaviť. Riešenie je však jednoduché, nazýva sa weak capturing. Skrátka povieme našej closure, aby capturing vykonávala cez weak referencie, vďaka čomu nemôže retain cycle nastať.

V kóde to vyzerá nasledovne:

DispatchQueue.global().async { [weak self] in

}

V CLOSURES sú pred in typicky vypísané parametre, ak je closure prijíma. Do hranatých zátvoriek môžeme špecifikovať, akým štýlom chceme capturing vykonať. V tomto prípade máme na self iba weak referenciu, takže pri použití je nutné použiť self?, Pretože self môže byť nil. Podobne môžeme vykonať capturing s akoukoľvek ďalšie premennou, len pozor na to, že weak treba špecifikovať pre každú premennú v hranatých zátvorkách, inak Swift použije tradičné strong capturing.

S [weak self] sa nám pri použití zmení iba nutnosť využiť ? operátor, pre prípad, že by už self bolo nil. Kód pre obdobné obnovenie UIImageView by teda mohol vyzerať nasledovne:

DispatchQueue.global().async { [weak self] in
                  self?.downloadData()

                  DispatchQueue.main.async {
                   // aktualizace UIImageView
                   // [weak self] podruhé není nutné
                  }
}

Mohli by sme tiež využiť konštrukciu guard a to nasledovne:

DispatchQueue.global().async { [weak self] in

                  guard self = self else { return }
                  self.downloadData()
}

Weak vs. unowned

Okrem weak ste možno zahliadli tiež kľúčové slovíčko unowned. To sa používa rovnako, ale funguje podobne ako ! s Optional. K premenným môžeme pristupovať rovnako, ako keby unowned modifikátor nemali, ak je ale niektorá nil, tak nám aplikácie jednoducho spadne. Za seba by som odporučil vždy používať weak.

A sme u konca prvej lekcie. V tej ďalšej, PerformSelector (), run loop a paralelné cyklus vo Swift , si ukážeme druhú a jednoduchšia možnosť, ako vykonať kód na pozadí. Vysvetlíme si, čo je run loop v aplikácii, k čomu je dobré o ňom mať potuchy a ďalšie špeciality DispatchQueue.


 

Všetky články v sekcii
Paralelné programovanie a viacvláknové aplikácie vo Swift
Preskočiť článok
(neodporúčame)
PerformSelector (), run loop a paralelné cyklus vo Swift
Článok pre vás napísal Filip Němeček
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity