Ahoj! Jmenuji se Kiryl Famin a jsem vývojář pro iOS. V tomto článku pokryjeme jednou provždy. Ačkoli se GCD může nyní, když existuje Swift Modern Concurrency, zdát zastaralé, kód využívající tento rámec se bude objevovat ještě mnoho let – jak v produkci, tak v rozhovorech. Grand Central Dispatch (GCD) Dnes se zaměříme pouze na základní porozumění GCD. Podrobně prozkoumáme pouze klíčové aspekty multithreadingu, včetně — téma, které mnoho jiných článků obvykle přehlíží. S těmito koncepty pro vás bude snazší porozumět tématům, jako je , , semafor, mutex a tak dále. vztahu mezi frontami a vlákny DispatchGroup DispatchBarrier Tento článek bude užitečný pro začátečníky i zkušené vývojáře. Pokusím se vše vysvětlit jasným jazykem a vyhnout se přetížení odbornými termíny. Přehled obsahu Základní pojmy: vlákno, multithreading, GCD, úloha, fronta Typy front: hlavní, globální, vlastní Priority fronty: Quality of Service (QoS) Sériové a souběžné fronty Způsoby provádění úloh: asynchronní, synchronizace Zablokování GCD cvičení Odkazy Základní pojmy: vlákno, vícevláknové zpracování, GCD, úloha a fronta – v podstatě kontejner, kde je umístěna a vykonávána sada systémových instrukcí. Ve skutečnosti veškerý spustitelný kód běží na nějakém vláknu. Rozlišujeme hlavní vlákno a pracovní vlákna. Vlákno – schopnost systému spouštět několik vláken současně (současně). To umožňuje paralelní běh více větví kódu. Multithreading – framework, který usnadňuje práci s vlákny (využívá výhody multithreadingu). Jeho hlavní primitiva jsou úkoly a fronty. Grand Central Dispatch (GCD) GCD je tedy nástroj, který usnadňuje psaní kódu, který se spouští souběžně. Jednoduchým příkladem je přesunutí náročných výpočtů do samostatného vlákna, aby nedošlo k narušení aktualizací uživatelského rozhraní v hlavním vlákně. – sada pokynů seskupených vývojářem. Je důležité pochopit, že vývojář rozhoduje, který kód patří ke konkrétní úloze. Úkol Například: print(“GCD”) // a task let database = Database() let person = Person(age: 23) // also a task database.store(person) – základní primitiv GCD, je to místo, kam vývojář zadává úkoly k provedení. Fronta přebírá odpovědnost za distribuci úloh mezi (každá fronta má přístup k systémové oblasti vláken). Fronta vlákna Fronty vám v podstatě umožňují soustředit se na uspořádání kódu do úloh, spíše než přímou správu vláken. Když odešlete úlohu do fronty, bude provedena v dostupném vláknu – často odlišném od toho, které bylo použito k odeslání úlohy. Můžete najít mp4 verze všech GIFů nebo v sekci „Odkazy“ níže. zde Typy front – fronta, která se spouští pouze v hlavním vláknu. Je sériový (o tom později). Hlavní fronta let mainQueue = DispatchQueue.main Globální fronty – systém poskytuje 5 front (jedna pro každou úroveň priority). Jsou souběžné. let globalQueue = DispatchQueue.global() Vlastní fronty – fronty vytvořené vývojářem. Vývojář si vybere jednu z 5 priorit a typ: sériový nebo souběžný (ve výchozím nastavení jsou sériové). let userQueue = DispatchQueue(label: “com.kirylfamin.concurrent”, attributes: .concurrent). Priority fronty – systém pro priority fronty. Čím vyšší prioritu má fronta, ve které je úkol zařazen, tím více zdrojů je pro něj alokováno. K dispozici je celkem 5 úrovní QoS: Quality of Service (QoS) – nejvyšší priorita. Používá se pro úlohy, které vyžadují okamžité provedení, ale nejsou vhodné pro spuštění v hlavním vláknu. Například v aplikaci, která umožňuje retušování obrazu v reálném čase, musí být výsledek retušování vypočten okamžitě; pokud by se to však dělalo v hlavním vláknu, narušovalo by to aktualizace uživatelského rozhraní a ovládání gest, ke kterým dochází vždy v hlavním vláknu (např. když uživatel přejede prstem po oblasti, která má být retušována, a aplikace musí okamžitě zobrazit výsledek „pod prstem“). Výsledek tak získáme co nejrychleji, aniž bychom zatížili hlavní vlákno. .userInteractive – priorita pro úkoly, které vyžadují rychlou zpětnou vazbu, i když nejsou tak důležité jako interaktivní úkoly. Obvykle se používá pro úlohy, kdy uživatel chápe, že úloha nebude dokončena okamžitě a bude muset čekat (například požadavek serveru). .userInitiated – standardní priorita. Přiřadí se, pokud vývojář při vytváření fronty nespecifikuje QoS – když pro úlohu neexistují žádné specifické požadavky a její prioritu nelze určit z kontextu (například pokud vyvoláte úlohu z fronty s prioritou .userInitiated, úloha tuto prioritu zdědí). .default – priorita pro úkoly, které nevyžadují okamžitou zpětnou vazbu od uživatele, ale jsou nezbytné pro fungování aplikace. Například synchronizace dat se serverem nebo zápis automatického ukládání na disk. .utility – nejnižší priorita. Příkladem je čištění mezipaměti. .background Všechny fronty jsou klasifikovány jako nebo sériové fronty souběžné fronty – jak název napovídá, jedná se o fronty, kde se úlohy provádějí jedna po druhé. To znamená, že další úloha začíná až po . Sériové fronty dokončení aktuálního – Tyto fronty umožňují provádění úloh paralelně – nová úloha se spustí, jakmile jsou přiděleny prostředky, bez ohledu na to, zda byly dokončeny předchozí úlohy. Pamatujte, že je zaručeno pouze pořadí zahájení (úloha zařazená dříve bude před pozdější), ale pořadí dokončení není zaručeno. Souběžné fronty zahájena Jak provádět úkoly Je důležité poznamenat, že nyní diskutujeme o metodách provádění úloh ve vztahu k . Jinými slovy, způsob, jakým voláte úlohu, určuje, jak se události vyvinou ve vláknu, ze kterého odešlete úlohu do fronty. volajícímu vláknu ( ) Asynchronně async Asynchronní volání je takové, kde volající vlákno není blokováno – to znamená, že nečeká na provedení úlohy, kterou zařadí do fronty. DispatchQueue.main.async { print(“A”) } print(“B”) V tomto příkladu asynchronně zařazujeme úlohu do fronty k v hlavní z hlavního (protože tento kód není v žádné konkrétní frontě, je standardně spuštěn v hlavním vláknu). Nečekáme tedy na úlohu v hlavním a ihned pokračujeme v provádění. V tomto konkrétním příkladu je úloha zařazena do fronty v hlavní a poté je v hlavním okamžitě spuštěna . Protože hlavní je zaneprázdněno prováděním aktuálního kódu (a úlohy z hlavní fronty se mohou provádět pouze v hlavním vlákně), aktuální úloha skončí jako první a teprve poté, co je hlavní volné, se spustí úloha zařazená do fronty v hlavní . Výstup je: BA. print("A") frontě vlákna vláknu print("A") frontě vlákně print("B") vlákno print("B") vlákno print("A") frontě DispatchQueue.global().async { updateData() DispatchQueue.main.async { updateInterface() } Logger.log(.success) } indicateLoading() Asynchronně přidáváme úlohu do globální fronty s výchozí prioritou z hlavního vlákna – takže volající vlákno okamžitě pokračuje a volá . indicateLoading() Po nějaké době systém alokuje prostředky pro úlohu a provede ji na volném pracovním vláknu z fondu vláken a zavolá se . updateData() Úloha obsahující je zařazena asynchronně do hlavní fronty – volající pracovní vlákno nečeká na své dokončení a pokračuje. updateInterface() Protože úkoly jsou zařazeny do fronty asynchronně, nemůžeme si být jisti, kdy budou zdroje přiděleny. V tomto případě nemůžeme s jistotou říci, zda se (v hlavním vláknu) nebo (na pracovním vláknu) provede jako první (ani nemůžeme v krocích 1-2: který se spustí jako první, v hlavním vláknu nebo v pracovním vláknu). I když je hlavní vlákno zaneprázdněné zpracováváním aktualizací uživatelského rozhraní, zpracováním gest a dalšími základními úkoly, vždy dostává maximum systémových prostředků. Na druhou stranu lze prostředky pro spuštění na pracovním vláknu alokovat také téměř okamžitě. updateInterface() Logger.log(.success) indicateLoading() updateData() Všimněte si, že v této animaci globální fronta vykonává své úkoly na nějakém volném pracovním vláknu ( ) Synchronně sync Synchronní volání je takové, kde se volající vlákno zastaví a čeká na dokončení úlohy, kterou zařadilo do fronty. let userQueue = DispatchQueue(label: "com.kirylfamin.serial") DispatchQueue.global().async { var account = BankAccount() userQueue.sync { account.balance += 10 } let balance = account.balance print(balance) } Zde z vlákna provádějícího úkol v globální frontě synchronně zařazujeme úkol do vlastní fronty, abychom zvýšili zůstatek. Aktuální vlákno je zablokováno a čeká na dokončení úlohy zařazené do fronty. Zůstatek se tedy vytiskne až poté, co úkol ve vlastní frontě dokončí přírůstek. pracovního Poznámka: Ve výše uvedené animaci provádí vlastní fronta své úkoly v některém volném pracovním vláknu Zablokování V kontextu synchronních úloh je důležité diskutovat o uváznutí – kdy vlákno nebo vlákna nekonečně dlouho čekají na sebe nebo na sebe, aby pokračovaly. Nejběžnějším příkladem je volání DispatchQueue.main.sync {} z hlavního . vlákna Hlavní je zaneprázdněno prováděním aktuální úlohy, v rámci které chceme synchronně spustit nějaký kód. Synchronní volání tedy blokuje hlavní . Úloha je zařazena do fronty v hlavní ale nelze ji spustit, protože hlavní je zablokováno a čeká na dokončení aktuální úlohy – a úlohy v hlavní lze spouštět pouze v hlavním . To může být zpočátku těžké si představit, ale klíčové je pochopit, že úloha zařazená do fronty s se stává součástí aktuální úlohy a my ji řadíme do fronty za aktuální úlohou. Výsledkem je, že vlákno čeká na část aktuální úlohy, kterou nelze spustit, protože vlákno je obsazeno aktuální úlohou. vlákno vlákno frontě, vlákno frontě vláknu DispatchQueue.main.sync func printing() { print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) } Všimněte si, že print("B") z hlavní fronty nelze provést, protože hlavní vlákno je blokováno. Cvičení GCD V této části se všemi dosud získanými znalostmi probereme cvičení různé složitosti: od jednoduchých bloků kódu, se kterými se setkáte při pohovorech, až po pokročilé výzvy, které posouvají vaše chápání souběžného programování. Otázka ve všech těchto úlohách zní: Co bude vytištěno na konzoli? Pamatujte, že hlavní fronta je sériová, fronty global() jsou souběžné a někdy může problém zahrnovat vlastní fronty se specifickými atributy. Základní cvičení Začneme úlohami normální obtížnosti – těmi s malou pravděpodobností nejistoty ve výstupu. Tyto úkoly se nejčastěji objevují v pohovorech; klíčem je věnovat čas a pečlivě analyzovat problém. Úplný kód všech cvičení naleznete . zde Úkol 1 print(“A”) DispatchQueue.main.async { print(“B”) } print(“C”) V hlavním vláknu se provede . print("A") Úloha k je zařazena asynchronně do hlavní fronty. Protože je hlavní vlákno zaneprázdněné, čeká tato úloha ve frontě. print("B") V hlavním vláknu se provede . print("C") Když je hlavní vlákno volné (po dokončení předchozí úlohy se mohou v hlavním vlákně vyskytnout další události, které vyžadují zpracování – nejen úlohy z hlavní fronty, jako jsou aktualizace uživatelského rozhraní, manipulace s gesty atd. Pro podrobnější pochopení si prosím přečtěte více o ), provede se . RunLoop print("B") : ACB Odpověď Úkol 2 print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”) V hlavním vláknu se provede . print("A") Úloha k je zařazena do hlavní fronty. Hlavní fronta, dokud nebude k dispozici hlavní vlákno. print("B") Úloha k je zařazena do fronty po tisku("B") a také čeká. print("C") Hlavní vlákno pokračuje ve vykonávání a tiskne "D". Když bude hlavní vlákno dostupné (po zpracování jiných úloh RunLoop), provede se první operace ve frontě . print("B") Poté, co se hlavní vlákno opět uvolní (po zpracování dalších úloh RunLoop – v budoucnu tento detail vynechám, protože neovlivňuje celkové pořadí), se provede úloha k . print("C") : ADBC Odpověď Hned bych měl zmínit, že v některých příkladech trochu zjednoduším vysvětlení a vynechám skutečnost, že systém optimalizuje provádění synchronních volání, o čemž se budeme bavit později. Úkol 3 print(“A”) DispatchQueue.main.async { // 1 print(“B”) DispatchQueue.main.async { print(“C”) } DispatchQueue.global().sync { print(“D”) } DispatchQueue.main.sync { // 2 print(“E”) } } // 3 print(“F”) DispatchQueue.main.async { print(“G”) } se provede v hlavním vláknu. print("A") Asynchronní úloha (označená 1–3) je zařazena do fronty v hlavní frontě, aniž by blokovala aktuální (hlavní) vlákno. Hlavní vlákno pokračuje ve vykonávání a tiskne . "F" Operace je zařazena do fronty po předchozí úloze (kroky 1–3). print("G") Jakmile se hlavní vlákno uvolní, zahájí se provádění první operace ve frontě . print("B") Operace je pak zařazena do fronty v hlavní frontě (kde se aktuální úloha stále provádí a ji ve frontě následuje). Vzhledem k tomu, že se přidává asynchronně, nečekáme na jeho provedení a jedeme hned dál. print("C") print("G") Dále je operace zařazena do globální fronty. Protože je toto volání synchronní, počkáme, až jej provede globální fronta (může běžet na jakémkoli dostupném pracovním vláknu), než budeme pokračovat. print("D") Nakonec je operace zařazena do fronty v hlavní frontě. Protože je toto volání synchronní, musí být aktuální vlákno zablokováno, dokud nebude úloha dokončena. V hlavní frontě jsou však již úkoly a operace je přidána na konec, za nimi. Proto se tyto operace musí nejprve provést, než bude možné spustit . Ale hlavní vlákno je stále zaneprázdněno prováděním aktuální operace, takže nemůže přejít na další operace ve frontě. I když po aktuální operaci nebyly provedeny žádné operace pro tisk a , vlákno stále nemohlo pokračovat, protože aktuální operace (kroky 1–3) ještě nebyla dokončena. print("E") print("E") print("E") "G" "C" Pokud by bylo volání asynchronní, operace print("E") by byla jednoduše zařazena do fronty po operacích pro tisk a . "G" "C" : AFBD Odpověď (pokud bylo druhé volání ): AFBDGCE Alternativní odpověď async Úkol 4 let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”) serialQueue.async { // 1 print(“A”) serialQueue.sync { print(“B”) } print(“C”) } // 2 Úloha (kroky 1–2) je zařazena asynchronně do vlastní sériové fronty (ve výchozím nastavení jsou fronty sériové, protože jsme nepoužili atribut ). .concurrent Když systém přidělí prostředky, spustí se provádění a vytiskne se . "A" Ve stejné sériové frontě je zařazena synchronní úloha k . Protože je volání synchronní, vlákno blokuje čekání na své provedení. print("B") Protože je však fronta sériová a stále je zaneprázdněna vnější úlohou 1-2, nelze úlohu spustit, což má za následek uváznutí. print("B") : A, uváznutí Odpověď Tento příklad ukazuje, že k uváznutí může dojít v jakékoli sériové frontě – ať už jde o hlavní frontu nebo vlastní frontu. Úkol 5 Vyměňme sériovou frontu z předchozí úlohy za souběžnou. DispatchQueue.global().async { // 1 print("A") DispatchQueue.global().sync { print("B") } print("C") } // 2 Úloha (kroky 1–2) je zařazena asynchronně do globální (souběžné) fronty. Když jsou zdroje přiděleny, spustí se provádění a vytiskne se . "A" Provede se synchronní volání pro provedení ve stejné globální frontě, které zablokuje aktuální pracovní , dokud nebude úloha dokončena. print("B") vlákno V tomto případě, i když je vlákno blokováno, protože globální fronta je souběžná, může začít provádět další operaci, aniž by čekala na dokončení aktuální – jednoduše spuštěním v jiném vláknu. Volající vlákno tedy čeká na provedení úlohy v jiném pracovním vláknu. print("B") Po dokončení úlohy se počáteční volající vlákno odblokuje, vytiskne se . "C" : ABC Odpověď Úkol 6 print("A") DispatchQueue.main.async { // 1 print("B") DispatchQueue.main.async { // 2 print("C") DispatchQueue.main.async { // 3 print("D") DispatchQueue.main.sync { print("E") } } // 4 } // 5 DispatchQueue.global().sync { // 6 print("F") DispatchQueue.global().sync { print("G") } } // 7 print("H") } // 8 print("I") Hlavní vlákno tiskne . "A" Asynchronní úloha (kroky 1–8) je zařazena do fronty v hlavní frontě bez blokování aktuálního vlákna. Hlavní vlákno pokračuje a tiskne . "I" Později, když je hlavní vlákno volné, úloha zařazená do hlavní fronty zahájí provádění a vytiskne . "B" Další asynchronní úloha (kroky 2–5) je zařazena do fronty v hlavní frontě – neblokuje aktuální vlákno. Pokračujícím provádění na aktuálním vláknu se provede synchronní odeslání operace 6–7 do globální fronty – toto blokuje aktuální (hlavní) vlákno, dokud nebude úloha dokončena. Operace 6–7 se spustí na jiném vláknu a vytiskne . "F" Operace k je synchronně odeslána do globální fronty a blokuje aktuální pracovní vlákno, dokud nebude dokončeno. print("G") Vytiskne se a odblokuje se pracovní vlákno, ze kterého byla tato operace odeslána. "G" Dokončí se operace 6–7, odblokuje se vlákno, ze kterého bylo odesláno (hlavní vlákno), a vytiskne se . "H" Po dokončení operace 1–2 se provádění přesune na další operaci v hlavní frontě – operaci 2–5 – která začne a vytiskne . "C" Operace 3–4 je zařazena do fronty v hlavní frontě bez blokování vlákna. Jakmile aktuální operace (2–5) skončí, spustí se provádění další operace (3–4), tiskne se . "D" Operace k je synchronně odeslána do hlavní fronty a blokuje aktuální vlákno. print("G") Systém pak nekonečně dlouho čeká, až se operace a provede se v hlavním vlákně – protože vlákno je zablokováno, vede to k uváznutí. print("E") : AIBFGHCD, uváznutí Odpověď Středně pokročilá cvičení Úlohy střední obtížnosti zahrnují nejistotu. S takovými problémy se také setkáváme při pohovorech, i když zřídka. Úkol 7 DispatchQueue.global().async { print("A") } DispatchQueue.global().async { print("B") } je zařazen do fronty asynchronně v globální frontě – bez blokování aktuálního vlákna. print("A") Čekáme, až systém přidělí prostředky pro úlohu v globální frontě. Teoreticky k tomu může dojít kdykoli – dokonce i před provedením dalšího příkazu pro zařazení do fronty . V tomto konkrétním případě je do fronty nejprve přidán další úkol a teprve poté jsou zdroje přiděleny do globální fronty. K tomu dochází, protože hlavnímu vláknu je přiděleno nejvíce prostředků a další operace v hlavním vláknu je velmi lehká (pouze operace přidání úkolu) a v praxi probíhá rychleji než alokace prostředků v globální frontě. Opačné scénáře probereme v další části. print("B") je zařazen do globální fronty. print("B") Mezitím hlavní vlákno pokračuje, zatímco globální fronta čeká na přidělení prostředků. Když jsou zdroje dostupné, provedou se oba úkoly. Přestože tisk úlohy může začít dříve než , nemůžeme zaručit objednávku, protože tisk není atomická operace (v okamžiku, kdy se výstup objeví v konzole, je blízko konce operace). "A" "B" : (AB) Odpověď Závorky označují, že písmena se mohou objevit v libovolném pořadí: buď AB nebo BA. Úkol 8 print("A") DispatchQueue.main.async { print("B") } DispatchQueue.global().async { print("C") } Zde si můžeme být jisti pouze tím, že „A“ se vytiskne jako první. Nemůžeme přesně určit, zda se rychleji provede úloha v hlavní frontě nebo v globální frontě. : A(BC) Odpověď Úkol 9 DispatchQueue.global(qos: .userInteractive).async { print(“A”) } DispatchQueue.main.async { // 1 print(“B”) } a DispatchQueue.global(qos: .userInteractive).async { print(“A”) } print(“B”) // 1 Na jedné straně se v obou případech provede na hlavním vlákně. Také nemůžeme přesně určit, kdy budou globální frontě přiděleny zdroje, takže teoreticky může být vytištěno těsně před dosažením bodu označeného // 1 v hlavním vláknu. V praxi se však první úloha vždy vytiskne jako AB, zatímco druhá jako BA. Důvodem je to, že v prvním případě se provede alespoň v další iteraci RunLoop hlavního vlákna (nebo o několik iterací později), zatímco ve druhém případě je naplánováno tak, aby se spustil v aktuální iteraci RunLoop v hlavním vlákně. Objednávku však nemůžeme zaručit. print("B") "A" print("B") print("B") na oba úkoly: (AB) Odpověď Úkol 10 print("A") DispatchQueue.global().async { print("B") DispatchQueue.global().async { print("C") } print("D") } Je jasné, že začátek výstupu je . Po zařazení do fronty nemůžeme přesně určit, kdy pro něj budou přiděleny zdroje – tato úloha se může provést buď před nebo po . To se občas stává i v praxi. "AB" print("C") print("D") : AB(CD) Odpověď Úkol 11 let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { print(“A”) serialQueue.async { print(“B”) } print(“C”) } Opět nemůžeme přesně určit, kdy budou zdroje přiděleny pro tisk ("B") ve vlastní frontě. V praxi, protože hlavní vlákno má nejvyšší prioritu, "C" se obvykle vytiskne před "B", i když to není zaručeno. : A(BC) Odpověď Úkol 12 DispatchQueue.global().async { print("A") } print("B") sleep(1) print("C") Zde je zřejmé, že výstupem bude BAC, protože jednosekundový spánek zajišťuje, že globální fronta má dostatek času na alokaci zdrojů. Zatímco hlavní vlákno je blokováno spánkem (což byste neměli dělat v produkci), se provádí v jiném vlákně. print("A") : BAC Odpověď Úkol 13 DispatchQueue.main.async { print("A") } print("B") sleep(1) print("C") V tomto případě, protože je zařazen do fronty v hlavní frontě, lze jej spustit pouze v hlavním vláknu. Hlavní vlákno však pokračuje ve vykonávání kódu — tiskne , pak spí, pak tiskne . Teprve poté může RunLoop provést úlohu zařazenou do fronty. print("A") "B" "C" : BCA Odpověď Pokročilé úkoly Je nepravděpodobné, že byste se s těmito problémy setkali při pohovorech, ale jejich pochopení vám pomůže lépe pochopit GCD. Counter Třída se zde používá výhradně pro referenční sémantiku: final class Counter { var count = 0 } Úkol 14 let counter = Counter() DispatchQueue.global().async { DispatchQueue.main.async { print(counter.count) } for _ in (0..<100) { // 1 counter.count += 1 } } Zde lze vytisknout libovolné číslo mezi 0 a 100 v závislosti na tom, jak je hlavní vlákno zaneprázdněné. Jak víme, nemůžeme přesně předpovědět, kdy asynchronní úloha získá prostředky – může se to stát před, během nebo po smyčce na pracovním vláknu. : 0-100 Odpověď Úkol 15 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } DispatchQueue.global(qos: .userInteractive).async { print(“B”) } QoS nezaručuje, že fronta s vyšší prioritou obdrží prostředky rychleji, i když se o to iOS pokusí. V praxi je zde výstup (AB). : (AB) Odpověď Úkol 16 var count = 0 DispatchQueue.global(qos: . userInitiated).async { for _ in 0..<1000 { count += 1 } print(“A”) } DispatchQueue.global(qos: .userInteractive).async { for _ in 0..<1000 { count += 1 } print(“B”) } Protože nemůžeme vědět, které provádění začne jako první, ani v rámci 1000 operací nemůžeme určit, která úloha bude dokončena rychleji. : (AB) Odpověď Úkol 16.2 Jaký je výstup za předpokladu, že se operace začnou provádět současně? Vzhledem k tomu, že frontě .userInteractive je přiděleno více zdrojů, během 1000 operací bude provádění v této frontě vždy dokončeno rychleji. : BA Odpověď Úkol 17 Pomocí podobného přístupu můžeme upravit jakoukoli úlohu s nejistotou z předchozí části (například úloha 12): let counter = Counter() let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { serialQueue.async { print(counter.count) } for _ in 0..<100 { counter.count += 1 } } Lze vytisknout libovolné číslo mezi 0 a 100. Skutečnost, že lze vytisknout 0, potvrzuje, že v úloze 12 nemůžeme zaručit, že výstup bude vždy před , protože se v podstatě nic nezměnilo – pouze to, že smyčka je o něco náročnější na zdroje než tisk (všimněte si, že pouhé spuštění smyčky, dokonce ještě před jejím provedením, vedlo v praxi k naprosté nejistotě). "C" "B" : 0-100 Odpověď Úkol 18 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } print(“B”) DispatchQueue.global(qos: .userInteractive).async { print(“C”) } Zde nastává podobná situace. Teoreticky může probíhat rychleji než (pokud nahradíte něčím o něco těžším). V praxi se vždy vytiskne jako první. Skutečnost, že provedeme před zařazením do fronty značně zvyšuje pravděpodobnost, že bude vytištěno před , protože čas navíc strávený v hlavním vláknu často postačuje k tomu, aby fronta .userInitiated získala prostředky a provedla . To však není zaručeno a někdy se může písmeno vytisknout rychleji. Teoreticky tedy existuje úplná nejistota; v praxi to bývá B(CA). print("A") print("B") print("B") "B" print("B") print("C") "A" "C" print("B") print("A") "C" : (BCA) Odpověď Úkol 19 DispatchQueue.global().sync { print(Thread.current) } k synchronizaci uvádí: Dokumentace "V rámci optimalizace výkonu tato funkce spouští bloky v aktuálním vláknu, kdykoli je to možné, s jednou výjimkou: bloky odeslané do hlavní fronty pro odeslání vždy běží v hlavním vláknu." To znamená, že pro účely optimalizace se mohou synchronní volání provádět ve stejném vlákně, ze kterého byla volána (s výjimkou – úlohy, které jej používají, se vždy provádějí v hlavním vláknu). Vytiskne se tedy aktuální (hlavní) vlákno. main.sync : hlavní vlákno Odpověď Úkol 20 DispatchQueue.global().sync { // 1 print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) } Vytiskne se pouze protože dojde k uváznutí. Kvůli optimalizaci se úloha (označená 1) začne provádět v hlavním vláknu a pak volání vede k uváznutí. "A" main.sync : Á, uváznutí Odpověď Úkol 21 DispatchQueue.main.async { print("A") DispatchQueue.global().sync { print("B") } print("C") } Optimalizace způsobí, že úloha nebude zařazena do fronty, ale bude "spojena" do aktuálního prováděcího vlákna. Tedy kód: print("B") DispatchQueue.global().sync { print("B") } se stává ekvivalentem: print(“B”) : ABC Odpověď Z těchto úloh je zřejmé, že main.sync musíte používat velmi opatrně – pouze když jste si jisti, že volání není provedeno z hlavního vlákna. Závěr V tomto článku jsme se zaměřili na základní koncepty multithreadingu v iOS – vlákna, úlohy a fronty – a jejich vzájemné vztahy. Prozkoumali jsme, jak GCD spravuje provádění úloh v hlavních, globálních a vlastních frontách, a diskutovali jsme o rozdílech mezi sériovým a souběžným prováděním. Kromě toho jsme zkoumali kritické rozdíly mezi synchronním (synchronním) a asynchronním (asynchronním) odesíláním úloh, přičemž jsme zdůraznili, jak tyto přístupy ovlivňují pořadí a načasování provádění kódu. Zvládnutí těchto základních konceptů je nezbytné pro vytváření citlivých, stabilních aplikací a pro vyhnutí se běžným nástrahám, jako jsou uváznutí. Doufám, že jste v tomto článku našli něco užitečného. Pokud bude cokoliv nejasné, neváhejte mě kontaktovat pro bezplatné vysvětlení na Telegramu: . @kfamyn Relevantní odkazy YouTube kanál se všemi animacemi - https://www.youtube.com/@kirylfamin Úplný kód cvičení - https://github.com/kfamyn/GCD-Tasks Můj telegram - http://t.me/kfamyn RunLoop – https://developer.apple.com/documentation/foundation/runloop Dokumentace k metodě - sync https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw