Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 1 - Úvod do vícevláknových aplikací ve Swift

Vítejte u první lekce Swift kurzu, který vás naučí, jak ve vašich iOS, macOS či dalších aplikacích napsaných v programovacím jazyce Swift využít maximální potenciál zařízení. Současné iPhone i iPad modely disponují několika procesorovými jádry, nemluvně o strojích s macOS. Ve výchozím stavu ale náš program vždy využije pouze jedno jádro. To může znamenat třeba čtvrtinu dostupného výkonu.

A12 Bionic - Paralelní programování a vícevláknové aplikace ve Swift

Přeci mega výkon A12 Bionic nechcete nechat ležet ladem, ne? :-)

Využití více vláken (multithreading) je ale důležité z mnohem podstatnějšího důvodu, než zapřáhnutí všech dostupných jader procesoru. Aplikace mají standardně jedno hlavní vlákno (označované jako Main nebo UI thread), které zkrátka provádí náš veškerý kód.

Toto hlavní vlákno má za úkol aplikaci spustit, vykreslit uživatelské rozhraní, provést naše metody a spoustu dalšího. Stejně tak uživatelské rozhraní následně obsluhuje, takže pokud například v iOS aplikaci chceme scrollovat komponentou TableView, na pozadí je to opět práce pro hlavní vlákno, aby vše korektně obstaralo.

Co myslíte, že se stane s aplikací, když bude toto hlavní vlákno zaneprázdněno čímkoliv jiným? Zkuste popřemýšlet...

Aplikace se jednoduše bude tvářit zaseknutá do doby, než hlavní vlákno dokončí předchozí práci a vrátí se k obsluze uživatelského rozhraní. Asi není třeba diskutovat nad tím, že tuto situaci v aplikacích prostě mít nechceme. Uživatelům je nepříjemné, když na ně aplikace nereaguje :)

Asi nejčastěji se nám podobná situace může stát, když budeme stahovat data z internetu. Klidně to při vývoji ani nemusíme zjistit, protože stahujeme pár kilobajtů JSON dat a máme vždy rychlý a spolehlivý internet. Hlavní vlákno tak data stáhne prakticky okamžitě k zaseknutí aplikace nedojde.

Jenže v praxi můžeme narazit na zahlcený server, kterému bude odpověď trvat třeba dvě sekundy. Nebo zrovna mobil uživatele bude připojen k přetížené WiFi či na EDGE mobilní připojení a rázem máme zaseknutou nebo zasekanou aplikaci (pokud stahujeme více dat).

Hello GCD!

Ve Swift není vícevláknové programování tak komplikované, jak může z názvu znít. Ve spoustě jazyků musí programátoři vytvářet vlákna jako objekty, pak se starat o jejich spuštění, dokončení a kdoví co ještě. Ve Swift sice lze třídu Thread vyhrabat, ale prakticky se nepoužívá. Máme totiž mnohem lepší řešení.

Seznamte se s GCD, již brzy vašim pomocníkem pro krocení vláken. Za zkratkou se ukrývá Grand Central Dispatcher a jeho úkolem je spravovat vlákna a zbavit programátora složitostí. Ve většině případů budeme pouze GCD úkolovat, že určitý kód chceme provést na pozadí (background thread) a nějaký jiný zas na hlavním vlákně. To je celé.

K hlavním vláknu si musíme říci ještě jednu důležitou věc. Říká se mu také UI thread, protože má na starost uživatelské rozhraní. To již víme. Důležité ale je, že pouze toto hlavní vlákno může "na uživatelské rozhraní sahat".

Jednoduše nemůžeme měnit vlastnosti UI komponent, animovat je či ovládat z ostatních vláken. Aplikace prostě spadne. Často vás ale Xcode upozorní právě na tento problém, takže není příliš záludný na odhalení.

DispatchQueue

Teorie už bylo dost, tak si pojďme ukázat skutečný kód pro práci s GCD. Nejzákladnější provedení kódu na pozadí může vypadat takto:

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

A to je celé! DispatchQueue slouží ke komunikaci s GCD a pomocí global().async za nás automaticky provede jakýkoliv kód ve vlákně na pozadí. Nás vlastně ani nezajímá, jaké vlákno to bude, jestli si GCD vytvoří nové nebo využije nějaké již vytvořené. Prostě provedeme kód na pozadí, nezablokujeme uživatelské rozhraní a nenašteveme uživatele :-)

DispatchQueue.main

Výše zmíněný kód je sice velmi jednoduchý, v praxi ale mnohdy stačit nebude. Často totiž chceme nějaká data stáhnout a následně je uživateli zobrazit. A jak již víme, manipulovat s uživatelským rozhraním může pouze hlavní vlákno aplikace.

To není problém, jednoduše opět využijeme DispatchQueue, ovšem tentokrát DispatchQueue.main a potřebné úpravy provedeme zde. Vypadat to může následovně:

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

Tímto se "přepneme" zpět na hlavní vlákno a upravíme třeba UIImageView, do kterého načteme nově stažená data.

QoS neboli Quality of Service

GCD také můžeme říci, jak je pro nás provedení daného kódu důležité a ten se podle toho zařídí. Opět se používá metoda global(), ale s parametrem určujícím právě QoS. Používají se čtyři volby v závislosti na tom, co chceme provést:

  • .userInteractive - nejvýkonnější varianta, používá se v případě interaktivní práce s aplikací.
  • .userInitiated - pro provedení akcí, které si vyžádal uživatel. Například export dat z aplikace, načtení dalších informací
  • .utility - pro činnosti, které mohou zabrat delší čas a nevyžaduje se okamžité provedení. Často se používá na stahování/nahrávání dat.
  • .background - nejnižší priorita, typicky pro činnosti, o kterých uživatel neví. To může být indexování nebo např. provádění zálohy.

Tyto volby potom použijeme následovně:

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

}

Může být lákavé vždy použít nejvýkonnější .userInteractive, ale nedělejte to. Pokud totiž zrovna uživatel bude ve vaší aplikaci dělat něco náročného a vy na pozadí spustíte třeba indexování s .userInteractive, můžete aplikaci snadno výrazně zpomalit.

Pozor na retain cycle

Teď je třeba provést delší odbočku od samotného GCD a probrat nutné základy o tzv. closure capturing. Již víme, že closure je jednoduše kus kódu, který můžeme uložit do proměnné a tak ho předávat a spouštět. Tak si můžeme připravit činnosti k provedení na později, např. obsluhu nějaké události apod.

Problém může nastat, když naše closure potřebuje k fungování externí proměnné, třeba celý ViewController, protože potřebuje zavolat některou z jeho metod. Dojde ke zmiňovanému closure capturing, což znamená, že si closure tyto proměnné "chytne" a drží si je, aby je mohla následně použít.

Podívejme se na následující 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)

Jaký podle vás bude výsledek? Kolik vypíše print() schovaný v closure? Pokud jste odpověděli, že 6, tak to není správně. Správná odpověď je 8, protože se proměnná counter před zavoláním closure změnila. Můžete si příklad zkusit v Playground.

Retain cycle

Closure capturing může velmi snadno vytvořit retain cycle, což znamená, že dva objekty drží reference na sebe a tím pádem nikdy nedojde k jejich dealokaci. Velký počet takových objektů může pak způsobit vyčerpání paměti a pád aplikace. Normálně je objekt dealokován (uvolněn z paměti), jakmile na něj neukazují žádné reference. Respektive ty silné (čtěte výchozí). Ve Swiftu používáme také weak reference, které se v případě dealokace nepočítají. Toto bychom měli znát a jde jen o opakování.

Retain cycle se může velmi snadno stát dost nepříjemným problémem a třeba ho ani nemusíte objevit. Řešení je ovšem snadné, nazývá se weak capturing. Zkrátka řekneme naší closure, aby capturing prováděla přes weak reference, díky čemuž nemůže retain cycle nastat.

V kódu to vypadá následovně:

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

}

V closures jsou před in typicky vypsané parametry, pokud je closure přijímá. Do hranatých závorek můžeme specifikovat, jakým stylem chceme capturing provést. V tomto případě máme na self pouze weak referenci, takže při použití je nutné použít self?, protože self může být nil. Podobně můžeme provést capturing s jakoukoliv další proměnnou, jen pozor na to, že weak je třeba specifikovat pro každou proměnnou v hranatých závorkách, jinak Swift použije tradiční strong capturing.

S [weak self] se nám při použití změní pouze nutnost využít ? operátor, pro případ, že by již self bylo nil. Kód pro obdobné obnovení UIImageView by tedy mohl vypadat následovně:

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

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

Mohli bychom také využít konstrukci guard a to následovně:

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

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

Weak vs. unowned

Kromě weak jste možná zahlédli také klíčové slovíčko unowned. To se používá stejně, ale funguje podobně jako ! s Optional. K proměnným můžeme přistupovat stejně, jako kdyby unowned modifikátor neměly, pokud je ale některá nil, tak nám aplikace prostě spadne. Za sebe bych doporučil vždy používat weak.

A jsme u konce první lekce.

V další lekci, PerformSelector(), run loop a paralelní cyklus ve Swift, si ukážeme druhou a jednodušší možnost, jak provést kód na pozadí. Vysvětlíme si, co je run loop v aplikaci, k čemu je dobré o něm mít ponětí a další speciality DispatchQueue.


 

Všechny články v sekci
Paralelní programování a vícevláknové aplikace ve Swift
Přeskočit článek
(nedoporučujeme)
PerformSelector(), run loop a paralelní cyklus ve Swift
Článek pro vás napsal Filip Němeček
Avatar
Uživatelské hodnocení:
3 hlasů
Autor se věnuje vývoji iOS aplikací (občas macOS)
Aktivity