Po pierwsze, ważne jest, aby znać różnicę między wątkami i kolejkami oraz tym, co naprawdę robi GCD. Kiedy używamy kolejek wysyłkowych (poprzez GCD), tak naprawdę robimy kolejkowanie, a nie wątkowanie. Framework Dispatch został zaprojektowany specjalnie po to, aby odciągnąć nas od wątków, ponieważ Apple przyznaje, że „wdrożenie prawidłowego rozwiązania wątkowego [może] stać się niezwykle trudne, jeśli nie [czasami] niemożliwe do osiągnięcia”. Dlatego, aby wykonywać zadania jednocześnie (zadania, których nie chcemy zamrażać interfejsu użytkownika), wystarczy utworzyć kolejkę tych zadań i przekazać ją do GCD. GCD obsługuje wszystkie powiązane wątki. Dlatego tak naprawdę wszystko, co robimy, to kolejkowanie.
Drugą rzeczą, którą należy od razu wiedzieć, jest to, czym jest zadanie. Zadanie to cały kod w tym bloku kolejki (nie w kolejce, ponieważ możemy dodawać rzeczy do kolejki przez cały czas, ale w ramach zamknięcia, w którym dodaliśmy je do kolejki). Zadanie jest czasami określane jako blok, a blok jest czasami określany jako zadanie (ale są one częściej nazywane zadaniami, szczególnie w społeczności Swift). Bez względu na to, ile lub mało kodu, cały kod w nawiasach klamrowych jest traktowany jako jedno zadanie:
serialQueue.async {
// this is one task
// it can be any number of lines with any number of methods
}
serialQueue.async {
// this is another task added to the same queue
// this queue now has two tasks
}
I jest oczywiste wspomnieć, że współbieżność oznacza po prostu w tym samym czasie z innymi rzeczami, a serial oznacza jeden po drugim (nigdy w tym samym czasie). Serializowanie czegoś lub umieszczanie czegoś w serialu oznacza po prostu wykonywanie tego od początku do końca w kolejności od lewej do prawej, od góry do dołu, nieprzerwanie.
Istnieją dwa typy kolejek, szeregowe i współbieżne, ale wszystkie kolejki są współbieżne względem siebie . Fakt, że chcesz uruchamiać dowolny kod „w tle” oznacza, że chcesz go uruchamiać jednocześnie z innym wątkiem (zwykle głównym wątkiem). Dlatego wszystkie kolejki wysyłania, seryjne lub współbieżne, wykonują swoje zadania współbieżnie względem innych kolejek . Każda serializacja wykonywana przez kolejki (przez kolejki szeregowe) dotyczy tylko zadań w ramach tej pojedynczej [seryjnej] kolejki wysyłkowej (tak jak w powyższym przykładzie, w którym istnieją dwa zadania w tej samej kolejce szeregowej; te zadania będą wykonywane jedno po drugi, nigdy jednocześnie).
KOLEJKI SZEREGOWE (często nazywane prywatnymi kolejkami wysyłkowymi) gwarantują wykonanie zadań pojedynczo od początku do końca w kolejności, w jakiej zostały dodane do tej konkretnej kolejki. Jest to jedyna gwarancja serializacji w każdym miejscu w omówieniu kolejek wysyłki - że określone zadania w ramach określonej kolejki szeregowej są wykonywane szeregowo. Kolejki szeregowe mogą jednak działać jednocześnie z innymi kolejkami szeregowymi, jeśli są oddzielnymi kolejkami, ponieważ znowu wszystkie kolejki są względem siebie współbieżne. Wszystkie zadania są uruchamiane w różnych wątkach, ale nie gwarantuje się, że każde zadanie zostanie uruchomione w tym samym wątku (nie jest to ważne, ale warto wiedzieć). A framework iOS nie zawiera żadnych gotowych do użycia kolejek szeregowych, musisz je utworzyć. Prywatne (nieglobalne) kolejki są domyślnie szeregowe, więc aby utworzyć kolejkę szeregową:
let serialQueue = DispatchQueue(label: "serial")
Możesz uczynić go współbieżnym poprzez jego właściwość atrybutu:
let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])
Ale w tym momencie, jeśli nie dodajesz żadnych innych atrybutów do kolejki prywatnej, Apple zaleca użycie jednej z ich gotowych do użycia globalnych kolejek (wszystkie są współbieżne). U dołu tej odpowiedzi zobaczysz inny sposób tworzenia kolejek szeregowych (przy użyciu właściwości target), zgodnie z zaleceniami Apple (w celu wydajniejszego zarządzania zasobami). Ale na razie wystarczy oznakowanie.
CONCURRENT QUEUES (często nazywane globalnymi kolejkami wysyłania) mogą wykonywać zadania jednocześnie; zadania mają jednak zagwarantować , że zostaną zainicjowane w kolejności, w jakiej zostały dodane do tej konkretnej kolejki, ale w przeciwieństwie do kolejek szeregowych kolejka nie czeka na zakończenie pierwszego zadania przed rozpoczęciem drugiego zadania. Zadania (podobnie jak kolejki szeregowe) są uruchamiane w odrębnych wątkach i (podobnie jak w przypadku kolejek szeregowych) nie gwarantuje się, że każde zadanie zostanie uruchomione w tym samym wątku (nie jest to ważne, ale warto wiedzieć). Struktura iOS zawiera cztery gotowe do użycia współbieżne kolejki. Możesz utworzyć kolejkę współbieżną, korzystając z powyższego przykładu lub używając jednej z globalnych kolejek Apple (co jest zwykle zalecane):
let concurrentQueue = DispatchQueue.global(qos: .default)
ODPORNOŚĆ NA CYKL ZATRZYMANIA: kolejki wysyłek są obiektami liczonymi jako odwołania, ale nie ma potrzeby zachowywania i zwalniania kolejek globalnych, ponieważ są one globalne, a zatem zachowywanie i zwalnianie jest ignorowane. Możesz uzyskać dostęp do kolejek globalnych bezpośrednio, bez konieczności przypisywania ich do właściwości.
Istnieją dwa sposoby wysyłania kolejek: synchronicznie i asynchronicznie.
SYNC DISPATCHING oznacza, że wątek, do którego została wysłana kolejka (wątek wywołujący), zatrzymuje się po wysłaniu kolejki i czeka na zakończenie wykonywania zadania w tym bloku kolejki przed wznowieniem. Aby wysłać synchronicznie:
DispatchQueue.global(qos: .default).sync {
// task goes in here
}
ASYNC DISPATCHING oznacza, że wątek wywołujący kontynuuje działanie po wysłaniu kolejki i nie czeka na zakończenie wykonywania zadania w tym bloku kolejki. Aby wysłać asynchronicznie:
DispatchQueue.global(qos: .default).async {
// task goes in here
}
Teraz można by pomyśleć, że aby wykonać zadanie szeregowo, należy użyć kolejki szeregowej, a to nie do końca prawda. Aby wykonać wiele zadań szeregowo, należy użyć kolejki szeregowej, ale wszystkie zadania (izolowane same) są wykonywane szeregowo. Rozważmy ten przykład:
whichQueueShouldIUse.syncOrAsync {
for i in 1...10 {
print(i)
}
for i in 1...10 {
print(i + 100)
}
for i in 1...10 {
print(i + 1000)
}
}
Bez względu na to, jak skonfigurujesz (szeregowo lub współbieżnie) lub wysyłasz (synchronizuj lub asynchronicznie) tę kolejkę, to zadanie zawsze będzie wykonywane szeregowo. Trzecia pętla nigdy nie będzie działać przed drugą pętlą, a druga pętla nigdy nie będzie działać przed pierwszą pętlą. Dotyczy to każdej kolejki korzystającej z dowolnej wysyłki. Dzieje się tak, gdy wprowadzasz wiele zadań i / lub kolejek, gdzie serial i współbieżność naprawdę wchodzą w grę.
Rozważ te dwie kolejki, jedną szeregową i jedną równoległą:
let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)
Powiedzmy, że wysyłamy dwie współbieżne kolejki w trybie asynchronicznym:
concurrentQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
103
3
104
4
105
5
Ich dane wyjściowe są pomieszane (zgodnie z oczekiwaniami), ale zauważ, że każda kolejka wykonywała swoje własne zadanie szeregowo. To najbardziej podstawowy przykład współbieżności - dwa zadania działające jednocześnie w tle w tej samej kolejce. Teraz zróbmy pierwszy serial:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
101
1
2
102
3
103
4
104
5
105
Czy pierwsza kolejka nie powinna być wykonywana szeregowo? Było (i było drugie). Cokolwiek wydarzyło się w tle, nie ma znaczenia dla kolejki. Powiedzieliśmy kolejce szeregowej, aby wykonywała szeregowo i tak się stało ... ale daliśmy jej tylko jedno zadanie. Teraz dajmy mu dwa zadania:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
I to jest najbardziej podstawowy (i jedyny możliwy) przykład serializacji - dwa zadania działające szeregowo (jedno po drugim) w tle (do głównego wątku) w tej samej kolejce. Ale jeśli utworzyliśmy dla nich dwie oddzielne kolejki szeregowe (ponieważ w powyższym przykładzie są to ta sama kolejka), ich dane wyjściowe są ponownie pomieszane:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue2.async {
for i in 1...5 {
print(i + 100)
}
}
1
101
2
102
3
103
4
104
5
105
I to właśnie miałem na myśli, mówiąc, że wszystkie kolejki są względem siebie współbieżne. Są to dwie kolejki szeregowe wykonujące swoje zadania w tym samym czasie (ponieważ są to oddzielne kolejki). Kolejka nie zna innych kolejek lub ich nie obchodzi. Wróćmy teraz do dwóch kolejek szeregowych (z tej samej kolejki) i dodajmy trzecią kolejkę, współbieżną:
serialQueue.async {
for i in 1...5 {
print(i)
}
}
serialQueue.async {
for i in 1...5 {
print(i + 100)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 1000)
}
}
1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005
To trochę nieoczekiwane, dlaczego kolejka współbieżna czekała na zakończenie kolejki szeregowej, zanim została wykonana? To nie jest współbieżność. Twój plac zabaw może pokazywać inny wynik, ale mój pokazał to. Pokazało to, ponieważ priorytet mojej kolejki współbieżnej nie był wystarczająco wysoki, aby GCD mógł wykonać swoje zadanie wcześniej. Więc jeśli zachowam wszystko bez zmian, ale zmienię QoS globalnej kolejki (jej jakość usług, która jest po prostu poziomem priorytetu kolejki) let concurrentQueue = DispatchQueue.global(qos: .userInteractive)
, wynik będzie zgodny z oczekiwaniami:
1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105
Dwie kolejki szeregowe wykonywały swoje zadania szeregowo (zgodnie z oczekiwaniami), a kolejka współbieżna wykonywała swoje zadanie szybciej, ponieważ otrzymała wysoki priorytet (wysoki QoS lub jakość usług).
Dwie równoległe kolejki, tak jak w naszym pierwszym przykładzie drukowania, pokazują pomieszany wydruk (zgodnie z oczekiwaniami). Aby sprawić, że będą drukować porządnie w trybie szeregowym, musielibyśmy utworzyć obie z tej samej kolejki szeregowej (również ta sama instancja tej kolejki, a nie tylko ta sama etykieta) . Następnie każde zadanie jest wykonywane szeregowo względem drugiego. Jednak innym sposobem, aby zmusić je do drukowania seryjnego, jest zachowanie ich obu jednocześnie, ale zmiana ich metody wysyłania:
concurrentQueue.sync {
for i in 1...5 {
print(i)
}
}
concurrentQueue.async {
for i in 1...5 {
print(i + 100)
}
}
1
2
3
4
5
101
102
103
104
105
Pamiętaj, że wysyłanie synchronizacji oznacza tylko, że wątek wywołujący czeka na zakończenie zadania w kolejce przed kontynuowaniem. Zastrzeżenie w tym miejscu polega oczywiście na tym, że wątek wywołujący jest zamrożony do czasu zakończenia pierwszego zadania, co może, ale nie musi, być zgodne z oczekiwaniami interfejsu użytkownika.
Z tego powodu nie możemy wykonać następujących czynności:
DispatchQueue.main.sync { ... }
Jest to jedyna możliwa kombinacja kolejek i metod wysyłania, której nie możemy wykonać - wysyłanie synchroniczne na głównej kolejce. A to dlatego, że prosimy główną kolejkę o zatrzymanie się, dopóki nie wykonamy zadania w nawiasach klamrowych ... które wysłaliśmy do głównej kolejki, którą właśnie zamroziliśmy. Nazywa się to impasem. Aby zobaczyć to w akcji na placu zabaw:
DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock
Ostatnią rzeczą, o której należy wspomnieć, są zasoby. Kiedy przydzielamy kolejce zadanie, GCD znajduje dostępną kolejkę z jej wewnętrznie zarządzanej puli. Jeśli chodzi o pisanie tej odpowiedzi, na QoS są dostępne 64 kolejki. Może się wydawać, że to dużo, ale można je szybko skonsumować, zwłaszcza przez biblioteki innych firm, w szczególności struktury baz danych. Z tego powodu Apple ma zalecenia dotyczące zarządzania kolejkami (wymienione w poniższych linkach); jedna istota:
Zamiast tworzyć prywatne współbieżne kolejki, prześlij zadania do jednej z globalnych współbieżnych kolejek wysyłkowych. W przypadku zadań szeregowych ustaw cel kolejki szeregowej na jedną z globalnych kolejek współbieżnych.
W ten sposób można zachować serializowane zachowanie kolejki, jednocześnie minimalizując liczbę oddzielnych kolejek tworzących wątki.
Aby to zrobić, zamiast tworzyć je tak, jak robiliśmy to wcześniej (co nadal możesz), Apple zaleca tworzenie takich kolejek szeregowych:
let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))
Do dalszej lektury polecam:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1
https://developer.apple.com/documentation/dispatch/dispatchqueue