paint-brush
Grand Central Dispatch, raz na zawszeprzez@kfamyn
1,011 odczyty
1,011 odczyty

Grand Central Dispatch, raz na zawsze

przez Kiryl Famin20m2025/02/28
Read on Terminal Reader

Za długo; Czytać

W tym artykule omówiono szczegółowo język Swift Grand Central Dispatch (GCD), wyjaśniając wątki, kolejki i bloki kodu, a także kwestie wykonywania kodu synchronizowanego i asynchronicznego, QoS i blokad. W celu utrwalenia wiedzy zastosowano ćwiczenia praktyczne.
featured image - Grand Central Dispatch, raz na zawsze
Kiryl Famin HackerNoon profile picture
0-item
1-item

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.

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

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.

Wątki w aplikacji


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.


Kolejka


Można znaleźć wersje mp4 wszystkich GIF-ów Tutaj lub w sekcji „Linki” poniżej.

Rodzaje kolejek

  1. 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


  2. Kolejki globalne – system udostępnia 5 kolejek (po jednej dla każdego poziomu priorytetu). Są one współbieżne.

     let globalQueue = DispatchQueue.global()


  3. 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

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:


  1. .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.


  2. .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).


  3. .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).


  4. .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.


  5. .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.

    Kolejki szeregowe i współbieżne


Jak wykonywać zadania

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.

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 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()


  1. 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() .


  2. 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() .


  3. 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.


  4. 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.



Wywołanie asynchroniczne


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 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.



Zadanie synchroniczne


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 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”) } 

Impas


Należy pamiętać, że nie można wykonać 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”)


  1. W wątku głównym wykonywane jest print("A") .
  2. Zadanie print("B") jest asynchronicznie umieszczane w kolejce głównej. Ponieważ wątek główny jest zajęty, to zadanie czeka w kolejce.
  3. W wątku głównym wykonywane jest print("C") .
  4. 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 RunLoop ), wykonywane jest umieszczone w kolejce zadanie print("B") .


Odpowiedź : ACB


Zadanie 2

 print(“A”) DispatchQueue.main.async { print(“B”) } DispatchQueue.main.async { print(“C”) } print(“D”)


  1. W wątku głównym wykonywane jest print("A") .
  2. Zadanie print("B") jest umieszczane w kolejce głównej. Kolejka główna, dopóki wątek główny nie stanie się dostępny.
  3. Zadanie print("C") jest umieszczane w kolejce po print("B") i również oczekuje.
  4. Wątek główny kontynuuje wykonywanie i wyświetla „D”.
  5. Gdy wątek główny staje się dostępny (po obsłużeniu innych zadań RunLoop), wykonywana jest pierwsza operacja w kolejce print("B") .
  6. 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") .


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”) }


  1. print("A") jest wykonywana w wątku głównym.
  2. Zadanie asynchroniczne (oznaczone numerami 1–3) jest umieszczane w kolejce głównej bez blokowania bieżącego (głównego) wątku.
  3. Wątek główny kontynuuje wykonywanie i drukuje "F" .
  4. Operacja print("G") jest umieszczana w kolejce głównej po poprzednim zadaniu (kroki 1–3).
  5. Gdy wątek główny zostanie zwolniony, rozpoczyna się wykonywanie pierwszej operacji w kolejce print("B") .
  6. Operacja 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.
  7. Następnie operacja 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.
  8. Na koniec operacja 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.
  • Gdyby wywołanie było asynchroniczne, operacja print("E") zostałaby po prostu umieszczona w kolejce po operacjach drukowania "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

  1. Zadanie (kroki 1–2) jest asynchronicznie umieszczane w kolejce szeregowej niestandardowej (domyślnie kolejki są szeregowe, ponieważ nie użyliśmy atrybutu .concurrent ).

  2. Gdy system przydzieli zasoby, rozpoczyna się wykonywanie i zostaje wydrukowany kod "A" .
  3. W tej samej kolejce szeregowej, zadanie synchroniczne print("B") jest umieszczane w kolejce. Ponieważ wywołanie jest synchroniczne, wątek blokuje się, czekając na jego wykonanie.
  4. Ponieważ jednak kolejka jest szeregowa i nadal zajęta zewnętrznym zadaniem 1-2, zadanie 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


  1. Zadanie (kroki 1–2) jest asynchronicznie umieszczane w kolejce globalnej (współbieżnej).
  2. Po przydzieleniu zasobów rozpoczyna się wykonywanie i zostaje wydrukowane "A" .
  3. Wykonywane jest synchroniczne wywołanie w celu wykonania print("B") na tej samej kolejce globalnej, co blokuje bieżący wątek roboczy do czasu zakończenia zadania.
  4. 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 print("B") w innym wątku roboczym.
  5. Po zakończeniu zadania początkowy wątek wywołujący zostaje odblokowany i zostaje wydrukowany napis "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")


  1. Wątek główny drukuje "A" .

  2. Zadanie asynchroniczne (kroki 1–8) jest umieszczane w kolejce głównej bez blokowania bieżącego wątku.

  3. Wątek główny jest kontynuowany i drukuje "I" .

  4. Później, gdy wątek główny jest wolny, zadanie umieszczone w kolejce głównej rozpoczyna wykonywanie i drukuje "B" .

  5. Kolejne zadanie asynchroniczne (kroki 2–5) jest umieszczane w kolejce głównej – nie blokując bieżącego wątku.

  6. 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.

  7. Operacje 6–7 rozpoczynają wykonywanie w innym wątku, drukując "F" .

  8. Operacja print("G") jest synchronicznie wysyłana do kolejki globalnej, blokując bieżący wątek roboczy do czasu jej zakończenia.

  9. Wyświetlany jest komunikat "G" , a wątek roboczy, z którego wysłano tę operację, zostaje odblokowany.

  10. 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" .

  11. 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" .

  12. Operacje 3–4 są umieszczane w kolejce głównej bez blokowania wątku.

  13. Po zakończeniu bieżącej operacji (2–5) rozpoczyna się wykonywanie następnej operacji (3–4), która wyświetla "D" .

  14. Operacja print("G") jest synchronicznie wysyłana do kolejki głównej, blokując bieżący wątek.

  15. 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

Ć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") }


  1. print("A") jest asynchronicznie umieszczana w kolejce globalnej — bez blokowania bieżącego wątku.
  2. 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 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.
  3. print("B") jest umieszczany w kolejce globalnej.
  4. W międzyczasie wątek główny kontynuuje działanie, podczas gdy kolejka globalna czeka na przydział zasobów.
  5. Gdy zasoby staną się dostępne, oba zadania zostaną wykonane. Chociaż zadanie drukowania "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

Zaawansowane zadania

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.

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

  1. Kanał YouTube ze wszystkimi animacjami - https://www.youtube.com/@kirylfamin
  2. Pełny kod ćwiczeń - https://github.com/kfamyn/GCD-Tasks
  3. Mój Telegram - http://t.me/kfamyn
  4. RunLoop — https://developer.apple.com/documentation/foundation/runloop
  5. Dokumentacja metody sync - https://developer.apple.com/documentation/dispatch/dispatchqueue/sync(execute:)-3segw