Lekce 2 - PerformSelector(), run loop a paralelní cyklus ve Swift
V minulé lekci, Úvod do vícevláknových aplikací ve Swift, jsme si vysvětlili proč potřebujeme
multithreading. Také již víme, že bychom pro něj měli využívat
primárně GCD a zapomenout, že existuje něco jako třída
Thread
, kterou můžeme využít a spustit pomocí ní něco na
pozadí.
Dnes budeme pokračovat se základními konstrukcemi okolo paralelního programování ve Swift.
Za dobu, co se iOS věnuji, jsem přístup pomocí
Thread
nikdy nepotkal a často blogové příspěvky o GCD
zkušených vývojářů přímo varovaly před používáním
Thread
.
performSelector()
Existuje ještě jedna alternativní možnost, jak na multithreading ve
Swift. A je velmi, velmi jednoduchá. Skrývá se v globálních metodách
performSelector()
, jejichž signatura určí, jestli se provedou na
pozadí nebo na hlavním vlákně.
Můžeme si rovnou ukázat příklad na kódu:
performSelector(inBackground: #selector(fetchData), with: nil)
Protože používáme #selector
, je nutné, aby volaná metoda,
v tomto případě fetchData()
, byla označena modifikátorem
@objc
. Ten ji zpřístupní pro Objective-C runtime, tedy
operační systém a ten se o zavolání postará. To si hned ukážeme.
Ačkoliv se jedná o řešení na jeden řádek kódu, není příliš
flexibilní. Posílání parametrů není přímočaré a pokud chceme s
výsledkem pracovat na hlavním vlákně, musíme toto "přepnutí" umístit do
volané metody. fetchData()
by mohlo tedy vypadat následovně:
@objc func fetchData() { let newData = apiService.getNewData() tableView.performSelector(onMainThread: #selector(UITableView.reloadData), with: nil, waitUntilDone: false) }
V této fiktivní ukázce slouží metoda fetchData()
ke
stažení dat z nějaké webové služby a tato data chceme uživateli
následně ukázat v komponentě UITableView
, takže ji musíme
aktualizovat. Protože se jedná o práci s UI, je třeba dostat se zpět na
hlavní vlákno. Volání performSelector()
je kapku odlišné,
protože nevoláme metodu z naší třídy (tedy našeho ViewControlleru), ale
chceme zavolat metodu komponenty UITableView
, takže
performSelector()
voláme na této komponentě a pomocí
#selector
vybereme metodu pro refresh dat. Fungovat to bude
korektně.
Teď si vzpomeňme na požadavek s modifikátorem
@objc
. Bez něj nebude #selector
fungovat. Jakmile
bychom tak chtěli volat metodu, která není naše (takže se nedá
modifikátor přidat) a nebo ho již neobsahuje, tak tento přístup fungovat
nebude. performSelector()
může být fajn volbou pro jednoduché
provedení metody na jiném než hlavním vlákně. Já bych doporučil
využívat spíše služby DispatchQueue
, častěji si ji díky
tomu připomenete a nabízí mnohonásobně větší flexibilitu.
Run loop
Když už se bavíme o DispatchQueue
a
performSelector()
, vysvětlíme si rovnou drobnost zvanou
application run loop. Run loop si můžete představit jako nekonečný cyklus,
který běží společně s aplikací s provádí veškerý kód na hlavním
vlákně.
Proč o tom mluvíme, když jsme mohli až do teď nerušeně programovat aplikace, aniž by nás run loop zajímal? Občas se při řešení problémů hodí vědět, že něco takového existuje. Když např. dojde ke stisknutí tlačítka, tak pro uživatele a i programátora je spuštění akce okamžité, pokud nejde o nějaký asynchronní kód. Uvnitř aplikace se ale děje něco trochu jiného. Kód navázaný na tlačítko poběží až v novém "kole" run loop. Ve zbytku současného, kdy bylo tlačítko spuštěno, totiž mimo jiné dojde k dokreslení animace stisku tlačítka. V opačném případě by mohlo dojít k zaseknutí.
Teď se právě dostáváme k DispatchQueue
a
performSelector()
. Jejich okamžité varianty totiž spustí
jakýkoliv kód až v následujícím "kole" run loop. Může to vypadat
následovně:
DispatchQueue.main.async {
// kód poběží v novém run loop
}
Ukažme si i příklad pro performSelector()
, metoda
someWork()
opět poběží až v následujícím run loop:
performSelector(onMainThread: #selector(someWork), with: nil, waitUntilDone: false)
Run loop se tu věnujeme primárně z důvodu, abyste minimálně tušili, o
co se jedná a měli představu, proč třeba cizí kód používá
DispatchQueue.main.async
, i když už běží na hlavním
vlákně.
S tímto problémem se můžeme často setkat u animací, které běží po
načtení aplikace. Třeba jsem narazil na situaci, že jsem komponentě
nastavil alpha = 0
v Interface Builder, abych mohl animovat fade-in
efekt při zapnutí aplikace a tuto animaci spustil v metodě
viewDidAppear()
. Může se stát, že k nastavení původní
alpha
na nula ještě nedošlo, UIKit uvidí, že nemá co
animovat, protože komponenta má stále hodnotu alpha
na
1
(tedy plně viditelná) a animace ji má posunout do stejného
stavu, takže animaci přeskočí. Není legrace něco takového hledat. A
právě zde vám posunutí kódu na další run loop pomůže.
Provedení kódu po uplynulé době
Občas v aplikacích potřebujeme provést nějaký kód po určitém časovém intervalu.
DispatchQueue
DispatchQueue
nám mimo jiné nabízí jednoduchý mechanismus,
jak něco provést až po uplynutí stanovené doby. Slouží k tomu metoda
asyncAfter()
, což je prakticky async
, ale s
parametrem určujícím jak dlouho se má před provedením čekat.
Její použití může vypadat následovně:
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { print("Uběhly 2 vteřiny!") }
Kromě .seconds
lze použít také .nanoseconds
,
.microseconds
a .miliseconds
. Nebo můžeme napsat
jednoduše .now() + 0.5
, kde 0.5
je počet sekund.
Tento zápis je častý, .seconds
je ale bez přemýšlení mnohem
jasnější. Samozřejmě asyncAfter()
lze použít také na
jakékoliv jiném queue, nemusí jít nutně o main
.
perform()
Využít můžeme rovněž metodu perform()
a v parametru zadat
v sekundách, jak dlouho má před provedením dané metody čekat. Nevýhodou
je, že musíme mít připravenou @objc
metodu, zatímco
asyncAfter()
na DispatchQueue
zvládne pracovat s
jakýmkoliv kódem.
Provedení metody po uplynulém čase s perform()
může vypadat
následovně:
perform(#selector(printMessage), with: nil, afterDelay: 2) @objc func printMessage() { print("Uběhly 2 vteřiny!") }
Paralelní cyklus jednoduše
DispatchQueue
nám nabízí snadnou možnost, jak libovolný
počet iterací provést paralelně. Příslušná metoda se jmenuje
concurrentPerform()
a jako parametr očekává pouze počet
iterací. K číslu iterace navíc dostaneme přístup v closure, takže
můžeme snadno existující for
cyklus převést na
concurrentPerform
.
Zápis vypadá třeba takto:
DispatchQueue.concurrentPerform(iterations: 100) { (i) in print(i) }
concurrentPerform()
může být skvělé v situacích, kdy na
hlavním vláknu potřebujeme provést složitější for
cyklus,
který má buď mnoho iterací nebo je každá iterace náročnějšího
charakteru. Pomocí této metody automaticky využijete maximum možností
procesoru.
Jen pozor na případ, kdy byste v iteracích potřebovali přistupovat ke sdíleným proměnným, protože to bude brzdit, jelikož vlákna budou muset čekat, až dojde k jejich uvolnění.
Tímto máme teoretickou část vícevláknového programování ve Swiftu za sebou.
Na praktickou ukázku se vrhneme v další lekci, Vytvoření iOS aplikace pro demonstraci GCD.