Cześć! Nazywam się Kiryl Famin i jestem programistą iOS.
W tym artykule raz na zawsze omówimy Grand Central Dispatch (GCD) . Chociaż GCD może wydawać się przestarzałe, skoro istnieje Swift Modern Concurrency, kod wykorzystujący ten framework będzie się pojawiał przez wiele lat — zarówno w produkcji, jak i w wywiadach.
Dzisiaj skupimy się wyłącznie na podstawowym zrozumieniu GCD. Szczegółowo przeanalizujemy tylko kluczowe aspekty wielowątkowości, w tym relację między kolejkami i wątkami — temat, który wiele innych artykułów ma tendencję do pomijania. Mając na uwadze te koncepcje, łatwiej będzie Ci zrozumieć tematy takie jak DispatchGroup
, DispatchBarrier
, semafor, mutex itd.
Ten artykuł będzie przydatny zarówno dla początkujących, jak i doświadczonych programistów. Postaram się wyjaśnić wszystko jasnym językiem, unikając przeładowania terminami technicznymi.
Wątek – zasadniczo kontener, w którym umieszczany jest zestaw instrukcji systemowych i wykonywany. W rzeczywistości cały kod wykonywalny jest uruchamiany w pewnym wątku. Rozróżniamy wątek główny i wątki robocze.
Wielowątkowość – zdolność systemu do wykonywania kilku wątków jednocześnie (w tym samym czasie). Umożliwia to równoległe wykonywanie wielu gałęzi kodu.
Grand Central Dispatch (GCD) – framework ułatwiający pracę z wątkami (wykorzystujący zalety wielowątkowości). Jego głównymi prymitywami są zadania i kolejki.
Zatem GCD jest narzędziem, które ułatwia pisanie kodu, który wykonuje się współbieżnie. Prostym przykładem jest odciążenie ciężkich obliczeń w osobnym wątku, aby nie kolidowały z aktualizacjami interfejsu użytkownika w wątku głównym.
Zadanie – zestaw instrukcji pogrupowanych przez programistę. Ważne jest, aby zrozumieć, że programista decyduje, który kod należy do konkretnego zadania.
Na przykład:
print(“GCD”) // a task
let database = Database() let person = Person(age: 23) // also a task database.store(person)
Kolejka – podstawowy prymityw GCD, miejsce, w którym programista umieszcza zadania do wykonania. Kolejka przejmuje odpowiedzialność za dystrybucję zadań pomiędzy wątkami (każda kolejka ma dostęp do puli wątków systemu).
Zasadniczo kolejki pozwalają skupić się na organizowaniu kodu w zadania, zamiast zarządzać wątkami bezpośrednio. Gdy wysyłasz zadanie do kolejki, zostanie ono wykonane w dostępnym wątku — często innym niż ten użyty do wysłania zadania.
Można znaleźć wersje mp4 wszystkich GIF-ów
Kolejka główna – kolejka, która wykonuje się tylko w wątku głównym. Jest szeregowa (więcej o tym później).
let mainQueue = DispatchQueue.main
Kolejki globalne – system udostępnia 5 kolejek (po jednej dla każdego poziomu priorytetu). Są one współbieżne.
let globalQueue = DispatchQueue.global()
Kolejki niestandardowe – kolejki tworzone przez dewelopera. Deweloper wybiera jeden z 5 priorytetów i typ: szeregowy lub współbieżny (domyślnie są szeregowe).
let userQueue = DispatchQueue(label: “com.kirylfamin.concurrent”, attributes: .concurrent).
Quality of Service (QoS) – system priorytetów kolejek. Im wyższy priorytet kolejki, w której znajduje się zadanie, tym więcej zasobów jest do niej przydzielanych. Łącznie istnieje 5 poziomów QoS:
.userInteractive
– najwyższy priorytet. Jest używany do zadań, które wymagają natychmiastowego wykonania, ale nie nadają się do uruchomienia w wątku głównym. Na przykład w aplikacji, która umożliwia retuszowanie obrazu w czasie rzeczywistym, wynik retuszu musi zostać obliczony natychmiast; jednak jeśli zostanie to wykonane w wątku głównym, będzie to kolidować z aktualizacjami interfejsu użytkownika i obsługą gestów, które zawsze występują w wątku głównym (np. gdy użytkownik przesuwa palec nad obszarem, który ma zostać poddany retuszowi, a aplikacja musi natychmiast wyświetlić wynik „pod palcem”). W ten sposób otrzymujemy wynik tak szybko, jak to możliwe, bez obciążania wątku głównego.
.userInitiated
– priorytet dla zadań wymagających szybkiej informacji zwrotnej, choć nie tak krytyczny jak zadania interaktywne. Jest on zazwyczaj używany do zadań, w których użytkownik rozumie, że zadanie nie zostanie ukończone natychmiast i będzie musiał poczekać (na przykład żądanie serwera).
.default
– standardowy priorytet. Jest przypisywany, jeśli deweloper nie określił QoS podczas tworzenia kolejki – gdy nie ma konkretnych wymagań dla zadania i jego priorytetu nie można określić na podstawie kontekstu (na przykład, jeśli wywołasz zadanie z kolejki z priorytetem .userInitiated, zadanie dziedziczy ten priorytet).
.utility
– priorytet dla zadań, które nie wymagają natychmiastowej informacji zwrotnej od użytkownika, ale są niezbędne do działania aplikacji. Na przykład synchronizowanie danych z serwerem lub zapisywanie autozapisu na dysku.
.background
– najniższy priorytet. Przykładem jest czyszczenie pamięci podręcznej.
Wszystkie kolejki są klasyfikowane jako kolejki szeregowe lub kolejki współbieżne
Kolejki szeregowe – jak sama nazwa wskazuje, są to kolejki, w których zadania są wykonywane jedno po drugim. Oznacza to, że następne zadanie rozpoczyna się dopiero po zakończeniu bieżącego .
Kolejki współbieżne – te kolejki umożliwiają równoległe wykonywanie zadań – nowe zadanie rozpoczyna się natychmiast po przydzieleniu zasobów, niezależnie od tego, czy poprzednie zadania zostały ukończone. Należy pamiętać, że zagwarantowana jest tylko kolejność rozpoczęcia (zadanie umieszczone wcześniej w kolejce rozpocznie się przed późniejszym), ale kolejność ukończenia nie jest gwarantowana.
Ważne jest, aby zauważyć, że omawiamy teraz metody wykonywania zadań w odniesieniu do wątku wywołującego . Innymi słowy, sposób, w jaki wywołujesz zadanie, determinuje, jak zdarzenia rozwijają się w wątku, z którego wysyłasz zadanie do kolejki.
async
)Wywołanie asynchroniczne to takie, w którym wątek wywołujący nie jest blokowany — innymi słowy, nie czeka na wykonanie zadania, które umieścił w kolejce.
DispatchQueue.main.async { print(“A”) } print(“B”)
W tym przykładzie asynchronicznie kolejkujemy zadanie print("A")
w kolejce głównej z wątku głównego (ponieważ ten kod nie znajduje się w żadnej konkretnej kolejce, jest domyślnie wykonywany w wątku głównym). Tak więc nie czekamy na zadanie w wątku głównym i kontynuujemy wykonywanie natychmiast. W tym konkretnym przykładzie zadanie print("A")
jest kolejkowane w kolejce głównej, a następnie print("B")
jest wykonywane natychmiast w wątku głównym. Ponieważ wątek główny jest zajęty wykonywaniem bieżącego kodu (a zadania z kolejki głównej mogą być wykonywane tylko w wątku głównym), bieżące zadanie print("B")
kończy się jako pierwsze i dopiero po zwolnieniu wątku głównego uruchamia się zadanie print("A")
umieszczone w kolejce głównej. Wyjście to: BA.
DispatchQueue.global().async { updateData() DispatchQueue.main.async { updateInterface() } Logger.log(.success) } indicateLoading()
Asynchronicznie dodajemy zadanie do kolejki globalnej z domyślnym priorytetem z wątku głównego — więc wątek wywołujący natychmiast kontynuuje działanie i wywołuje indicateLoading()
.
Po pewnym czasie system przydziela zasoby dla zadania i wykonuje je w wolnym wątku roboczym z puli wątków, po czym wywoływana jest updateData()
.
Zadanie zawierające updateInterface()
jest asynchronicznie umieszczane w kolejce głównej — wywołujący wątek roboczy nie czeka na jej zakończenie i kontynuuje działanie.
Ponieważ zadania są kolejkowane asynchronicznie, nie możemy być pewni, kiedy zasoby zostaną przydzielone. W tym przypadku nie możemy powiedzieć na pewno, czy updateInterface()
(w wątku głównym) lub Logger.log(.success)
(w wątku roboczym) zostanie wykonane jako pierwsze (nie możemy tego również powiedzieć w krokach 1-2: co wykonuje się jako pierwsze, indicateLoading()
w wątku głównym lub updateData()
w wątku roboczym). Chociaż wątek główny jest zajęty obsługą aktualizacji interfejsu użytkownika, przetwarzaniem gestów i innymi podstawowymi zadaniami, to jednak zawsze otrzymuje maksymalne zasoby systemowe. Z drugiej strony zasoby do wykonania w wątku roboczym mogą być również przydzielone niemal natychmiast.
Zwróć uwagę, że w tej animacji globalna kolejka wykonuje swoje zadania na pewnym wolnym wątku roboczym
sync
)Wywołanie synchroniczne to takie, w którym wątek wywołujący zatrzymuje się i czeka na zakończenie zadania, które umieścił w kolejce.
let userQueue = DispatchQueue(label: "com.kirylfamin.serial") DispatchQueue.global().async { var account = BankAccount() userQueue.sync { account.balance += 10 } let balance = account.balance print(balance) }
Tutaj, z wątku roboczego wykonującego zadanie w kolejce globalnej, synchronicznie umieszczamy zadanie w kolejce niestandardowej, aby zwiększyć saldo. Bieżący wątek jest blokowany i czeka na zakończenie zadania w kolejce. W ten sposób saldo jest drukowane dopiero po zakończeniu inkrementacji przez zadanie w kolejce niestandardowej.
Uwaga: W animacji powyżej kolejka niestandardowa wykonuje swoje zadania na pewnym wolnym wątku roboczym
W kontekście zadań synchronicznych ważne jest omówienie impasu — gdy wątek lub wątki czekają w nieskończoność na siebie lub na siebie nawzajem, aby kontynuować. Najczęstszym przykładem jest wywołanie DispatchQueue.main.sync {} z wątku głównego.
Główny wątek jest zajęty wykonywaniem bieżącego zadania, w ramach którego chcemy synchronicznie wykonać pewien kod. Dlatego wywołanie synchroniczne blokuje wątek główny. Zadanie jest umieszczane w kolejce głównej, ale nie może zostać uruchomione, ponieważ wątek główny jest zablokowany i czeka na zakończenie bieżącego zadania — a zadania w kolejce głównej mogą być uruchamiane tylko w wątku głównym. Na początku może być to trudne do wyobrażenia, ale kluczem jest zrozumienie, że zadanie umieszczone w kolejce za pomocą DispatchQueue.main.sync
staje się częścią bieżącego zadania, a my umieszczamy je w kolejce po bieżącym zadaniu. W rezultacie wątek czeka na część bieżącego zadania, która nie może zostać uruchomiona, ponieważ wątek jest zajęty przez bieżące zadanie.
func printing() { print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) }
Należy pamiętać, że nie można wykonać print("B")
z kolejki głównej, ponieważ wątek główny jest zablokowany.
W tej sekcji, z całą zdobytą dotychczas wiedzą, omówimy ćwiczenia o różnym stopniu złożoności: od prostych bloków kodu, na które natkniesz się podczas rozmów kwalifikacyjnych, po zaawansowane wyzwania, które poszerzą Twoje zrozumienie programowania współbieżnego. Pytanie we wszystkich tych zadaniach brzmi: Co zostanie wydrukowane na konsoli?
Należy pamiętać, że kolejka główna jest szeregowa, kolejki global() są współbieżne, a czasami problem może obejmować kolejki niestandardowe o określonych atrybutach.
Zaczniemy od zadań o normalnym stopniu trudności – tych, które mają niewielkie szanse na niepewność w wynikach. Te zadania są tymi, które najczęściej pojawiają się w wywiadach; kluczem jest poświęcenie czasu i uważna analiza problemu.
Pełny kod wszystkich ćwiczeń znajdziesz tutaj .
Zadanie 1
print(“A”) DispatchQueue.main.async { print(“B”) } print(“C”)
print("A")
.print("B")
jest asynchronicznie umieszczane w kolejce głównej. Ponieważ wątek główny jest zajęty, to zadanie czeka w kolejce.print("C")
.print("B")
.
Odpowiedź : ACB
Zadanie 2
print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”)
print("A")
.print("B")
jest umieszczane w kolejce głównej. Kolejka główna, dopóki wątek główny nie stanie się dostępny.print("C")
jest umieszczane w kolejce po print("B") i również oczekuje.print("B")
.print("C")
.
Odpowiedź : ADBC
Powinienem od razu wspomnieć, że w niektórych przykładach nieco uproszczę wyjaśnienie i pominę fakt, że system optymalizuje wykonywanie wywołań synchronicznych, o czym porozmawiamy później.
Zadanie 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”) }
print("A")
jest wykonywana w wątku głównym."F"
.print("G")
jest umieszczana w kolejce głównej po poprzednim zadaniu (kroki 1–3).print("B")
.print("C")
jest następnie umieszczana w kolejce głównej (gdzie bieżące zadanie jest nadal wykonywane, a print("G")
podąża za nim w kolejce). Ponieważ jest dodawana asynchronicznie, nie czekamy na jej wykonanie i przechodzimy dalej natychmiast.print("D")
jest umieszczana w kolejce globalnej. Ponieważ to wywołanie jest synchroniczne, czekamy, aż kolejka globalna je wykona (może być uruchomiona na dowolnym dostępnym wątku roboczym), zanim przejdziemy dalej.print("E")
jest umieszczana w kolejce głównej. Ponieważ to wywołanie jest synchroniczne, bieżący wątek musi zostać zablokowany do czasu zakończenia zadania. Jednak w kolejce głównej są już zadania, a operacja print("E")
jest dodawana na końcu, po nich. Dlatego te operacje muszą zostać wykonane jako pierwsze, zanim print("E")
będzie mogło zostać uruchomione. Jednak wątek główny jest nadal zajęty wykonywaniem bieżącej operacji, więc nie może przejść do następnych operacji w kolejce. Nawet jeśli nie było żadnych operacji drukowania "G"
i "C"
po bieżącej operacji, wątek nadal nie mógł kontynuować, ponieważ bieżąca operacja (kroki 1–3) nie została jeszcze ukończona."G"
i "C"
.
Odpowiedź : AFBD
Alternatywna odpowiedź (jeśli drugie wywołanie było async
): AFBDGCE
Zadanie 4
let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”) serialQueue.async { // 1 print(“A”) serialQueue.sync { print(“B”) } print(“C”) } // 2
Zadanie (kroki 1–2) jest asynchronicznie umieszczane w kolejce szeregowej niestandardowej (domyślnie kolejki są szeregowe, ponieważ nie użyliśmy atrybutu .concurrent
).
"A"
.print("B")
jest umieszczane w kolejce. Ponieważ wywołanie jest synchroniczne, wątek blokuje się, czekając na jego wykonanie.print("B")
nie może zostać uruchomione, co powoduje impas.
Odpowiedź : A, impas
Ten przykład pokazuje, że impas może wystąpić w dowolnej kolejce szeregowej — niezależnie od tego, czy jest to kolejka główna, czy niestandardowa.
Zadanie 5
Zastąpmy kolejkę szeregową z poprzedniego zadania kolejką współbieżną.
DispatchQueue.global().async { // 1 print("A") DispatchQueue.global().sync { print("B") } print("C") } // 2
"A"
.print("B")
na tej samej kolejce globalnej, co blokuje bieżący wątek roboczy do czasu zakończenia zadania.print("B")
w innym wątku roboczym."C"
.Odpowiedź : ABC
Zadanie 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")
Wątek główny drukuje "A"
.
Zadanie asynchroniczne (kroki 1–8) jest umieszczane w kolejce głównej bez blokowania bieżącego wątku.
Wątek główny jest kontynuowany i drukuje "I"
.
Później, gdy wątek główny jest wolny, zadanie umieszczone w kolejce głównej rozpoczyna wykonywanie i drukuje "B"
.
Kolejne zadanie asynchroniczne (kroki 2–5) jest umieszczane w kolejce głównej – nie blokując bieżącego wątku.
Kontynuując wykonywanie w bieżącym wątku, do kolejki globalnej wysyłana jest synchronicznie operacja 6–7, która blokuje bieżący (główny) wątek do czasu zakończenia zadania.
Operacje 6–7 rozpoczynają wykonywanie w innym wątku, drukując "F"
.
Operacja print("G")
jest synchronicznie wysyłana do kolejki globalnej, blokując bieżący wątek roboczy do czasu jej zakończenia.
Wyświetlany jest komunikat "G"
, a wątek roboczy, z którego wysłano tę operację, zostaje odblokowany.
Operacje 6–7 zostają ukończone, wątek, z którego zostały wysłane (wątek główny) zostaje odblokowany, a następnie drukowane jest "H"
.
Po zakończeniu operacji 1–2 wykonywanie przechodzi do następnej operacji w kolejce głównej — operacji 2–5 — która rozpoczyna się i drukuje "C"
.
Operacje 3–4 są umieszczane w kolejce głównej bez blokowania wątku.
Po zakończeniu bieżącej operacji (2–5) rozpoczyna się wykonywanie następnej operacji (3–4), która wyświetla "D"
.
Operacja print("G")
jest synchronicznie wysyłana do kolejki głównej, blokując bieżący wątek.
Następnie system czeka w nieskończoność, aż operacja print("E")
zostanie wykonana w wątku głównym — ponieważ wątek jest zablokowany, prowadzi to do impasu.
Odpowiedź : AIBFGHCD, impas
Zadania o średnim stopniu trudności wiążą się z niepewnością. Takie problemy występują również w wywiadach, choć rzadko.
Zadanie 7
DispatchQueue.global().async { print("A") } DispatchQueue.global().async { print("B") }
print("A")
jest asynchronicznie umieszczana w kolejce globalnej — bez blokowania bieżącego wątku.print("B")
. W tym konkretnym przypadku następne zadanie jest najpierw dodawane do kolejki, a dopiero potem zasoby są przydzielane do kolejki globalnej. Dzieje się tak, ponieważ wątek główny ma przydzielonych najwięcej zasobów, a następna operacja na wątku głównym jest bardzo lekka (tylko operacja dodania zadania) i w praktyce następuje szybciej niż przydział zasobów w kolejce globalnej. Przeciwne scenariusze omówimy w następnej sekcji.print("B")
jest umieszczany w kolejce globalnej."A"
może rozpocząć się wcześniej niż "B"
, nie możemy zagwarantować kolejności, ponieważ drukowanie nie jest operacją atomową (moment, w którym dane wyjściowe pojawią się w konsoli, jest bliski końca operacji).
Odpowiedź : (AB)
Nawiasy oznaczają, że litery mogą występować w dowolnej kolejności: AB lub BA.
Zadanie 8
print("A") DispatchQueue.main.async { print("B") } DispatchQueue.global().async { print("C") }
Tutaj możemy być pewni tylko tego, że „A” zostanie wydrukowane jako pierwsze. Nie możemy dokładnie określić, czy zadanie w kolejce głównej, czy w kolejce globalnej zostanie wykonane szybciej.
Odpowiedź : A(BC)
Zadanie 9
DispatchQueue.global(qos: .userInteractive).async { print(“A”) } DispatchQueue.main.async { // 1 print(“B”) }
I
DispatchQueue.global(qos: .userInteractive).async { print(“A”) } print(“B”) // 1
Z jednej strony, w obu przypadkach print("B")
jest wykonywane w wątku głównym. Ponadto, nie możemy dokładnie określić, kiedy globalna kolejka otrzyma przydzielone zasoby, więc teoretycznie, "A"
może zostać wydrukowane bezpośrednio przed osiągnięciem punktu oznaczonego // 1 w wątku głównym. W praktyce jednak, pierwsze zadanie zawsze drukuje jako AB, podczas gdy drugie drukuje jako BA. Dzieje się tak, ponieważ w pierwszym przypadku print("B")
jest wykonywane co najmniej w następnej iteracji RunLoop wątku głównego (lub kilka iteracji później), podczas gdy w drugim przypadku print("B")
jest zaplanowane do uruchomienia w bieżącej iteracji RunLoop wątku głównego. Jednak nie możemy zagwarantować kolejności.
Odpowiedź na oba zadania: (AB)
Zadanie 10
print("A") DispatchQueue.global().async { print("B") DispatchQueue.global().async { print("C") } print("D") }
Wiadomo, że początek wyjścia to "AB"
. Po umieszczeniu print("C")
w kolejce nie możemy dokładnie określić, kiedy zasoby zostaną dla niego przydzielone — to zadanie może zostać wykonane przed lub po print("D")
. W praktyce zdarza się to czasami.
Odpowiedź : AB(CD)
Zadanie 11
let serialQueue = DispatchQueue(label: “com.kirylfamin.serial”, qos: .userInteractive) DispatchQueue.main.async { print(“A”) serialQueue.async { print(“B”) } print(“C”) }
Ponownie, nie możemy dokładnie określić, kiedy zasoby zostaną przydzielone do print("B") w kolejce niestandardowej. W praktyce, ponieważ wątek główny ma najwyższy priorytet, "C" zwykle drukuje przed "B", choć nie jest to gwarantowane.
Odpowiedź : A(BC)
Zadanie 12
DispatchQueue.global().async { print("A") } print("B") sleep(1) print("C")
Tutaj jest oczywiste, że wyjście będzie BAC, ponieważ jednosekundowe uśpienie zapewnia, że globalna kolejka ma wystarczająco dużo czasu na przydzielenie zasobów. Podczas gdy wątek główny jest blokowany przez uśpienie (czego nie należy robić w środowisku produkcyjnym), print("A")
wykonuje się w innym wątku.
Odpowiedź : BAC
Zadanie 13
DispatchQueue.main.async { print("A") } print("B") sleep(1) print("C")
W tym przypadku, ponieważ print("A")
jest w kolejce głównej, może być wykonany tylko w wątku głównym. Jednak wątek główny kontynuuje wykonywanie kodu — drukując "B"
, następnie uśpiony, a następnie drukując "C"
. Dopiero po tym RunLoop może wykonać zadanie w kolejce.
Odpowiedź : BCA
Mało prawdopodobne, że napotkasz te problemy na rozmowach kwalifikacyjnych, jednak ich zrozumienie pomoże Ci lepiej zrozumieć GCD.
Klasa Counter jest tutaj używana wyłącznie w celach semantycznych:
final class Counter { var count = 0 }
Zadanie 14
let counter = Counter() DispatchQueue.global().async { DispatchQueue.main.async { print(counter.count) } for _ in (0..<100) { // 1 counter.count += 1 } }
Tutaj może zostać wydrukowana dowolna liczba od 0 do 100, w zależności od tego, jak zajęty jest wątek główny. Jak wiemy, nie możemy dokładnie przewidzieć, kiedy zadanie asynchroniczne otrzyma zasoby — może to nastąpić przed, w trakcie lub po pętli wątku roboczego.
Odpowiedź : 0-100
Zadanie 15
DispatchQueue.global(qos: .userInitiated).async { print(“A”) } DispatchQueue.global(qos: .userInteractive).async { print(“B”) }
QoS nie gwarantuje, że kolejka o wyższym priorytecie otrzyma zasoby szybciej, chociaż iOS spróbuje to zrobić. W praktyce wyjście tutaj to (AB).
Odpowiedź : (AB)
Zadanie 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”) }
Ponieważ nie wiemy, które zadanie zostanie wykonane jako pierwsze, nawet wśród 1000 operacji nie jesteśmy w stanie określić, które zadanie zostanie ukończone szybciej.
Odpowiedź : (AB)
Zadanie 16.2
Jaki będzie wynik przy założeniu, że operacje rozpoczną się jednocześnie?
Ponieważ kolejce .userInteractive przydzielono więcej zasobów, w przypadku wykonania 1000 operacji wykonywanie operacji w tej kolejce zawsze zakończy się szybciej.
Odpowiedź : BA
Zadanie 17
Stosując podobne podejście, możemy zmodyfikować dowolne zadanie z niepewnością z poprzedniej sekcji (na przykład Zadanie 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 } }
Można wydrukować dowolną liczbę od 0 do 100. Fakt, że można wydrukować 0, potwierdza, że w Zadaniu 12 nie możemy zagwarantować, że wyjście "C"
zawsze wystąpi przed "B"
, ponieważ zasadniczo nic się nie zmieniło — tylko to, że pętla wymaga nieco więcej zasobów niż wydruk (należy zauważyć, że samo rozpoczęcie pętli, nawet przed jej wykonaniem, w praktyce skutkowało całkowitą niepewnością).
Odpowiedź : 0-100
Zadanie 18
DispatchQueue.global(qos: .userInitiated).async { print(“A”) } print(“B”) DispatchQueue.global(qos: .userInteractive).async { print(“C”) }
Podobna sytuacja ma miejsce tutaj. Teoretycznie print("A")
może być wykonywane szybciej niż print("B")
(jeśli zastąpisz print("B")
czymś nieco cięższym). W praktyce "B"
zawsze drukuje się jako pierwsze. Jednak fakt, że wykonujemy print("B")
przed umieszczeniem print("C")
w kolejce, znacznie zwiększa prawdopodobieństwo, że "A"
zostanie wydrukowane przed "C"
, ponieważ dodatkowy czas spędzony na print("B")
w wątku głównym jest często wystarczający, aby kolejka .userInitiated mogła pobrać zasoby i wykonać print("A")
. Niemniej jednak nie jest to gwarantowane i czasami "C"
może być drukowane szybciej. Tak więc w teorii istnieje całkowita niepewność; w praktyce ma tendencję do B(CA).
Odpowiedź : (BCA)
Zadanie 19
DispatchQueue.global().sync { print(Thread.current) }
Dokumentacja synchronizacji podaje:
„W celu optymalizacji wydajności ta funkcja wykonuje bloki w bieżącym wątku, kiedy tylko jest to możliwe, z jednym wyjątkiem: bloki przesłane do głównej kolejki wysyłkowej zawsze są uruchamiane w wątku głównym”.
Oznacza to, że w celach optymalizacji wywołania synchroniczne mogą być wykonywane w tym samym wątku, z którego zostały wywołane (z wyjątkiem main.sync
– zadania używające go zawsze są wykonywane w wątku głównym). W ten sposób drukowany jest bieżący (główny) wątek.
Odpowiedź : wątek główny
Zadanie 20
DispatchQueue.global().sync { // 1 print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) }
Tylko "A"
jest drukowane, ponieważ występuje impas. Ze względu na optymalizację zadanie (oznaczone etykietą 1) rozpoczyna wykonywanie w wątku głównym, a następnie wywołanie main.sync
prowadzi do impasu.
Odpowiedź : A, impas
Zadanie 21
DispatchQueue.main.async { print("A") DispatchQueue.global().sync { print("B") } print("C") }
Optymalizacja powoduje, że zadanie print("B")
nie jest umieszczane w kolejce, ale jest „wplatane” w bieżący wątek wykonania. Tak więc kod:
DispatchQueue.global().sync { print("B") }
staje się równoważne:
print(“B”)
Odpowiedź : ABC
Z tych zadań jasno wynika, że z polecenia main.sync należy korzystać bardzo ostrożnie — tylko wtedy, gdy mamy pewność, że wywołanie nie jest wykonywane z wątku głównego.
W tym artykule skupiliśmy się na podstawowych koncepcjach wielowątkowości w systemie iOS — wątkach, zadaniach i kolejkach — oraz ich wzajemnych powiązaniach. Przyjrzeliśmy się, w jaki sposób GCD zarządza wykonywaniem zadań w kolejkach głównych, globalnych i niestandardowych, a także omówiliśmy różnice między wykonywaniem szeregowym i współbieżnym. Ponadto przeanalizowaliśmy krytyczne rozróżnienia między synchronicznym (sync) i asynchronicznym (async) wysyłaniem zadań, podkreślając, w jaki sposób te podejścia wpływają na kolejność i czas wykonywania kodu. Opanowanie tych podstawowych koncepcji jest niezbędne do tworzenia responsywnych, stabilnych aplikacji i unikania typowych pułapek, takich jak blokady.
Mam nadzieję, że znalazłeś coś przydatnego w tym artykule. Jeśli coś pozostaje niejasne, możesz skontaktować się ze mną, aby uzyskać bezpłatne wyjaśnienie na Telegramie: @kfamyn .
sync
- https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw