Cześć! Nazywam się Kiryl Famin i jestem programistą iOS. W tym artykule raz na zawsze omówimy . 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. Grand Central Dispatch (GCD) Dzisiaj skupimy się wyłącznie na podstawowym zrozumieniu GCD. Szczegółowo przeanalizujemy tylko kluczowe aspekty wielowątkowości, w tym — 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 , , semafor, mutex itd. relację między kolejkami i wątkami DispatchGroup DispatchBarrier 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. Przegląd treści Podstawowe koncepcje: wątek, wielowątkowość, GCD, zadanie, kolejka Typy kolejek: główna, globalna, niestandardowa Priorytety kolejki: Jakość usług (QoS) Kolejki szeregowe i współbieżne Sposoby wykonywania zadań: asynchroniczne, synchroniczne Impas Ćwiczenia NWW Spinki do mankietów Podstawowe koncepcje: wątek, wielowątkowość, GCD, zadanie i kolejka – 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. Wątek – 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. Wielowątkowość – framework ułatwiający pracę z wątkami (wykorzystujący zalety wielowątkowości). Jego głównymi prymitywami są zadania i kolejki. Grand Central Dispatch (GCD) 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. – zestaw instrukcji pogrupowanych przez programistę. Ważne jest, aby zrozumieć, że programista decyduje, który kod należy do konkretnego zadania. Zadanie Na przykład: print(“GCD”) // a task let database = Database() let person = Person(age: 23) // also a task database.store(person) – podstawowy prymityw GCD, miejsce, w którym programista umieszcza zadania do wykonania. Kolejka przejmuje odpowiedzialność za dystrybucję zadań pomiędzy (każda kolejka ma dostęp do puli wątków systemu). Kolejka wątkami 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 lub w sekcji „Linki” poniżej. Tutaj Rodzaje kolejek – kolejka, która wykonuje się tylko w wątku głównym. Jest szeregowa (więcej o tym później). Kolejka główna 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). Priorytety kolejek – 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: Quality of Service (QoS) – 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. .userInteractive – 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). .userInitiated – 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). .default – 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. .utility – najniższy priorytet. Przykładem jest czyszczenie pamięci podręcznej. .background Wszystkie kolejki są klasyfikowane jako lub kolejki szeregowe kolejki współbieżne – 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 . Kolejki szeregowe zakończeniu bieżącego – 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 przed późniejszym), ale kolejność ukończenia nie jest gwarantowana. Kolejki współbieżne rozpocznie się Jak wykonywać zadania Ważne jest, aby zauważyć, że omawiamy teraz metody wykonywania zadań w odniesieniu do . 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. wątku wywołującego ( ) Asynchronicznie 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 w głównej z 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 głównym i kontynuujemy wykonywanie natychmiast. W tym konkretnym przykładzie zadanie jest kolejkowane w głównej, a następnie jest wykonywane natychmiast w głównym. Ponieważ 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 kończy się jako pierwsze i dopiero po zwolnieniu głównego uruchamia się zadanie umieszczone w głównej. Wyjście to: BA. print("A") kolejce wątku wątku print("A") kolejce print("B") wątku wątek print("B") wątku print("A") kolejce 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 jest asynchronicznie umieszczane w kolejce głównej — wywołujący wątek roboczy nie czeka na jej zakończenie i kontynuuje działanie. updateInterface() Ponieważ zadania są kolejkowane asynchronicznie, nie możemy być pewni, kiedy zasoby zostaną przydzielone. W tym przypadku nie możemy powiedzieć na pewno, czy (w wątku głównym) lub (w wątku roboczym) zostanie wykonane jako pierwsze (nie możemy tego również powiedzieć w krokach 1-2: co wykonuje się jako pierwsze, w wątku głównym lub 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. updateInterface() Logger.log(.success) indicateLoading() updateData() Zwróć uwagę, że w tej animacji globalna kolejka wykonuje swoje zadania na pewnym wolnym wątku roboczym ( ) Synchronicznie 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 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. roboczego Uwaga: W animacji powyżej kolejka niestandardowa wykonuje swoje zadania na pewnym wolnym wątku roboczym Impas 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 głównego. wątku Główny jest zajęty wykonywaniem bieżącego zadania, w ramach którego chcemy synchronicznie wykonać pewien kod. Dlatego wywołanie synchroniczne blokuje główny. Zadanie jest umieszczane w głównej, ale nie może zostać uruchomione, ponieważ główny jest zablokowany i czeka na zakończenie bieżącego zadania — a zadania w głównej mogą być uruchamiane tylko w głównym. Na początku może być to trudne do wyobrażenia, ale kluczem jest zrozumienie, że zadanie umieszczone w kolejce za pomocą 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. wątek wątek kolejce wątek kolejce wątku DispatchQueue.main.sync func printing() { print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) } nie można wykonać Należy pamiętać, że print("B") z kolejki głównej, ponieważ wątek główny jest zablokowany. Ćwiczenia NWW 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. Podstawowe ćwiczenia 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”) W wątku głównym wykonywane jest . print("A") Zadanie jest asynchronicznie umieszczane w kolejce głównej. Ponieważ wątek główny jest zajęty, to zadanie czeka w kolejce. print("B") W wątku głównym wykonywane jest . print("C") Gdy wątek główny jest wolny (po zakończeniu poprzedniego zadania, mogą istnieć inne zdarzenia wymagające przetworzenia w wątku głównym — nie tylko zadania z kolejki głównej, takie jak aktualizacje interfejsu użytkownika, obsługa gestów itp. Aby uzyskać bardziej szczegółowe informacje, zapoznaj się z tematem ), wykonywane jest umieszczone w kolejce zadanie . RunLoop print("B") : ACB Odpowiedź Zadanie 2 print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”) W wątku głównym wykonywane jest . print("A") Zadanie jest umieszczane w kolejce głównej. Kolejka główna, dopóki wątek główny nie stanie się dostępny. print("B") Zadanie jest umieszczane w kolejce po print("B") i również oczekuje. print("C") Wątek główny kontynuuje wykonywanie i wyświetla „D”. Gdy wątek główny staje się dostępny (po obsłużeniu innych zadań RunLoop), wykonywana jest pierwsza operacja w kolejce . print("B") Po ponownym zwolnieniu wątku głównego (po obsłużeniu innych zadań RunLoop — w przyszłości pominę ten szczegół, ponieważ nie wpływa on na ogólną kolejność), wykonywane jest zadanie . print("C") : ADBC Odpowiedź 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”) } jest wykonywana w wątku głównym. print("A") Zadanie asynchroniczne (oznaczone numerami 1–3) jest umieszczane w kolejce głównej bez blokowania bieżącego (głównego) wątku. Wątek główny kontynuuje wykonywanie i drukuje . "F" Operacja jest umieszczana w kolejce głównej po poprzednim zadaniu (kroki 1–3). print("G") Gdy wątek główny zostanie zwolniony, rozpoczyna się wykonywanie pierwszej operacji w kolejce . print("B") Operacja jest następnie umieszczana w kolejce głównej (gdzie bieżące zadanie jest nadal wykonywane, a podąża za nim w kolejce). Ponieważ jest dodawana asynchronicznie, nie czekamy na jej wykonanie i przechodzimy dalej natychmiast. print("C") print("G") Następnie operacja 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("D") Na koniec operacja 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 jest dodawana na końcu, po nich. Dlatego te operacje muszą zostać wykonane jako pierwsze, zanim 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 i po bieżącej operacji, wątek nadal nie mógł kontynuować, ponieważ bieżąca operacja (kroki 1–3) nie została jeszcze ukończona. print("E") print("E") print("E") "G" "C" Gdyby wywołanie było asynchroniczne, operacja print("E") zostałaby po prostu umieszczona w kolejce po operacjach drukowania i . "G" "C" : AFBD Odpowiedź (jeśli drugie wywołanie było ): AFBDGCE Alternatywna odpowiedź async 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 Gdy system przydzieli zasoby, rozpoczyna się wykonywanie i zostaje wydrukowany kod . "A" W tej samej kolejce szeregowej, zadanie synchroniczne jest umieszczane w kolejce. Ponieważ wywołanie jest synchroniczne, wątek blokuje się, czekając na jego wykonanie. print("B") Ponieważ jednak kolejka jest szeregowa i nadal zajęta zewnętrznym zadaniem 1-2, zadanie nie może zostać uruchomione, co powoduje impas. print("B") : A, impas Odpowiedź 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 Zadanie (kroki 1–2) jest asynchronicznie umieszczane w kolejce globalnej (współbieżnej). Po przydzieleniu zasobów rozpoczyna się wykonywanie i zostaje wydrukowane . "A" Wykonywane jest synchroniczne wywołanie w celu wykonania na tej samej kolejce globalnej, co blokuje bieżący roboczy do czasu zakończenia zadania. print("B") wątek W tym przypadku, mimo że wątek jest zablokowany, ponieważ globalna kolejka jest współbieżna, może rozpocząć wykonywanie następnej operacji bez czekania na zakończenie bieżącej — po prostu uruchamiając ją w innym wątku. W ten sposób wątek wywołujący czeka na wykonanie zadania w innym wątku roboczym. print("B") Po zakończeniu zadania początkowy wątek wywołujący zostaje odblokowany i zostaje wydrukowany napis . "C" : ABC Odpowiedź 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 jest synchronicznie wysyłana do kolejki globalnej, blokując bieżący wątek roboczy do czasu jej zakończenia. print("G") Wyświetlany jest komunikat , a wątek roboczy, z którego wysłano tę operację, zostaje odblokowany. "G" 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 jest synchronicznie wysyłana do kolejki głównej, blokując bieżący wątek. print("G") Następnie system czeka w nieskończoność, aż operacja zostanie wykonana w wątku głównym — ponieważ wątek jest zablokowany, prowadzi to do impasu. print("E") : AIBFGHCD, impas Odpowiedź Ćwiczenia średniozaawansowane 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") } jest asynchronicznie umieszczana w kolejce globalnej — bez blokowania bieżącego wątku. print("A") Czekamy, aż system przydzieli zasoby dla zadania w kolejce globalnej. Teoretycznie może się to zdarzyć w dowolnym momencie — nawet przed wykonaniem następnego polecenia dodania do kolejki . 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. print("B") W międzyczasie wątek główny kontynuuje działanie, podczas gdy kolejka globalna czeka na przydział zasobów. Gdy zasoby staną się dostępne, oba zadania zostaną wykonane. Chociaż zadanie drukowania może rozpocząć się wcześniej niż , 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). "A" "B" : (AB) Odpowiedź 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. : A(BC) Odpowiedź 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 jest wykonywane w wątku głównym. Ponadto, nie możemy dokładnie określić, kiedy globalna kolejka otrzyma przydzielone zasoby, więc teoretycznie, 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 jest wykonywane co najmniej w następnej iteracji RunLoop wątku głównego (lub kilka iteracji później), podczas gdy w drugim przypadku jest zaplanowane do uruchomienia w bieżącej iteracji RunLoop wątku głównego. Jednak nie możemy zagwarantować kolejności. print("B") "A" print("B") print("B") na oba zadania: (AB) Odpowiedź Zadanie 10 print("A") DispatchQueue.global().async { print("B") DispatchQueue.global().async { print("C") } print("D") } Wiadomo, że początek wyjścia to . Po umieszczeniu w kolejce nie możemy dokładnie określić, kiedy zasoby zostaną dla niego przydzielone — to zadanie może zostać wykonane przed lub po . W praktyce zdarza się to czasami. "AB" print("C") print("D") : AB(CD) Odpowiedź 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. : A(BC) Odpowiedź 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), wykonuje się w innym wątku. print("A") : BAC Odpowiedź Zadanie 13 DispatchQueue.main.async { print("A") } print("B") sleep(1) print("C") W tym przypadku, ponieważ 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 , następnie uśpiony, a następnie drukując . Dopiero po tym RunLoop może wykonać zadanie w kolejce. print("A") "B" "C" : BCA Odpowiedź Zaawansowane zadania Mało prawdopodobne, że napotkasz te problemy na rozmowach kwalifikacyjnych, jednak ich zrozumienie pomoże Ci lepiej zrozumieć GCD. Counter Klasa 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. : 0-100 Odpowiedź 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). : (AB) Odpowiedź 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. : (AB) Odpowiedź 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. : BA Odpowiedź 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 zawsze wystąpi przed , 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ą). "C" "B" : 0-100 Odpowiedź Zadanie 18 DispatchQueue.global(qos: .userInitiated).async { print(“A”) } print(“B”) DispatchQueue.global(qos: .userInteractive).async { print(“C”) } Podobna sytuacja ma miejsce tutaj. Teoretycznie może być wykonywane szybciej niż (jeśli zastąpisz czymś nieco cięższym). W praktyce zawsze drukuje się jako pierwsze. Jednak fakt, że wykonujemy przed umieszczeniem w kolejce, znacznie zwiększa prawdopodobieństwo, że zostanie wydrukowane przed , ponieważ dodatkowy czas spędzony na w wątku głównym jest często wystarczający, aby kolejka .userInitiated mogła pobrać zasoby i wykonać . Niemniej jednak nie jest to gwarantowane i czasami może być drukowane szybciej. Tak więc w teorii istnieje całkowita niepewność; w praktyce ma tendencję do B(CA). print("A") print("B") print("B") "B" print("B") print("C") "A" "C" print("B") print("A") "C" : (BCA) Odpowiedź Zadanie 19 DispatchQueue.global().sync { print(Thread.current) } synchronizacji podaje: Dokumentacja „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 – 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. main.sync : wątek główny Odpowiedź Zadanie 20 DispatchQueue.global().sync { // 1 print(“A”) DispatchQueue.main.sync { print(“B”) } print(“C”) } Tylko 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 prowadzi do impasu. "A" main.sync : A, impas Odpowiedź Zadanie 21 DispatchQueue.main.async { print("A") DispatchQueue.global().sync { print("B") } print("C") } Optymalizacja powoduje, że zadanie nie jest umieszczane w kolejce, ale jest „wplatane” w bieżący wątek wykonania. Tak więc kod: print("B") DispatchQueue.global().sync { print("B") } staje się równoważne: print(“B”) : ABC Odpowiedź 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. Wniosek 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 Powiązane linki Kanał YouTube ze wszystkimi animacjami - https://www.youtube.com/@kirylfamin Pełny kod ćwiczeń - https://github.com/kfamyn/GCD-Tasks Mój Telegram - http://t.me/kfamyn RunLoop — https://developer.apple.com/documentation/foundation/runloop Dokumentacja metody - sync https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw