Kolejki równoległe i szeregowe w GCD

117

Staram się w pełni zrozumieć kolejki równoległe i szeregowe w GCD. Mam pewne problemy i mam nadzieję, że ktoś może mi odpowiedzieć jasno i na temat.

  1. Czytam, że kolejki szeregowe są tworzone i używane do wykonywania zadań jedna po drugiej. Jednak co się stanie, jeśli:

    • Tworzę kolejkę szeregową
    • Używam dispatch_async(w właśnie utworzonej kolejce szeregowej) trzy razy, aby wysłać trzy bloki A, B, C

    Czy trzy bloki zostaną wykonane:

    • w kolejności A, B, C, ponieważ kolejka jest szeregowa

      LUB

    • współbieżnie (w tym samym czasie na równoległych wątkach), ponieważ użyłem wysyłki ASYNC
  2. Czytam, że mogę używać dispatch_syncna współbieżnych kolejkach, aby wykonywać bloki jeden po drugim. W takim przypadku DLACZEGO w ogóle istnieją kolejki szeregowe, skoro zawsze mogę korzystać z kolejki współbieżnej, w której mogę wysyłać SYNCHRONICZNIE tyle bloków, ile chcę?

    Dzięki za dobre wyjaśnienie!

Bogdan Alexandru
źródło
Proste, dobre pytanie wstępne: synchronizacja wysyłania i asynchronizacja
miód

Odpowiedzi:

216

Prosty przykład: masz blok, którego wykonanie zajmuje minutę. Dodajesz go do kolejki z głównego wątku. Spójrzmy na cztery przypadki.

  • async - concurrent: kod działa w wątku w tle. Sterowanie wraca natychmiast do głównego wątku (i interfejsu użytkownika). Blok nie może zakładać, że jest to jedyny blok działający w tej kolejce
  • async - serial: kod działa w wątku w tle. Sterowanie wraca natychmiast do głównego wątku. Blok może zakładać, że jest to jedyny blok działający w tej kolejce
  • sync - concurrent: kod działa w wątku w tle, ale główny wątek czeka na jego zakończenie, blokując wszelkie aktualizacje interfejsu użytkownika. Blok nie może zakładać, że jest to jedyny blok działający w tej kolejce (mogłem dodać kolejny blok przy użyciu async kilka sekund wcześniej)
  • sync - serial: kod działa w wątku w tle, ale główny wątek czeka na zakończenie, blokując wszelkie aktualizacje interfejsu użytkownika. Blok może zakładać, że jest to jedyny blok działający w tej kolejce

Oczywiście nie używałbyś żadnego z dwóch ostatnich do długotrwałych procesów. Zwykle widzisz to, gdy próbujesz zaktualizować interfejs użytkownika (zawsze w głównym wątku) z czegoś, co może być uruchomione w innym wątku.

Stephen Darlington
źródło
14
Mówisz mi więc, że: (1) typ kolejki (conc lub serial) jest JEDYNYM elementem decydującym o tym, czy zadania są wykonywane w kolejności, czy równolegle ;; (2) typ wysyłki (synchronizacja lub asynchronizacja) mówi tylko o tym, czy wykonanie idzie, czy nie przechodzi do następnej instrukcji? Chodzi mi o to, że jeśli wyślę zadanie, SYNC kod będzie blokowany do zakończenia tego zadania, bez względu na kolejkę, w której jest wykonywany?
Bogdan Alexandru
13
@BogdanAlexandru Poprawnie. Kolejka określa zasady wykonywania, a nie sposób umieszczania bloku w kolejce. Synchronizacja czeka na zakończenie bloku, asynchronizacja nie.
Jano
2
@swiftBUTCHER Do pewnego momentu tak. Podczas tworzenia kolejki możesz określić maksymalną liczbę wątków. Jeśli dodasz mniej zadań, będą one wykonywane równolegle. Co więcej, niektóre zadania pozostaną w kolejce, dopóki nie będzie dostępnej pojemności.
Stephen Darlington
2
@PabloA., Głównym wątkiem jest kolejka szeregowa, więc tak naprawdę są tylko dwa przypadki. Poza tym jest dokładnie to samo. Async zwraca natychmiast (i blok prawdopodobnie zostanie wykonany na końcu bieżącej pętli uruchamiania). Głównym problemem jest synchronizacja z głównego wątku do głównego wątku, w którym to przypadku dochodzi do impasu.
Stephen Darlington,
1
@ShauketSheikh No. Głównym wątkiem jest kolejka szeregowa, ale nie wszystkie kolejki szeregowe są głównym wątkiem. W czwartym punkcie główny wątek blokowałby się, czekając, aż inny wątek ukończy swoją pracę. Gdyby kolejka szeregowa była głównym wątkiem, nastąpiłby zakleszczenie.
Stephen Darlington
122

Oto kilka eksperymentów, które przeprowadziłem, aby zrozumieć serial, czym są concurrentkolejki Grand Central Dispatch.

 func doLongAsyncTaskInSerialQueue() {

   let serialQueue = DispatchQueue(label: "com.queue.Serial")
      for i in 1...5 {
        serialQueue.async {

            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}

Zadanie będzie działać w innym wątku (innym niż główny), gdy użyjesz async w GCD. Async oznacza, że ​​wykonaj następną linię, nie czekaj do wykonania bloku, co skutkuje nieblokującym wątkiem głównym i główną kolejką. Ponieważ jest to kolejka szeregowa, wszystkie są wykonywane w kolejności, w jakiej są dodawane do kolejki szeregowej. Zadania wykonywane szeregowo są zawsze wykonywane pojedynczo przez pojedynczy wątek skojarzony z kolejką.

func doLongSyncTaskInSerialQueue() {
    let serialQueue = DispatchQueue(label: "com.queue.Serial")
    for i in 1...5 {
        serialQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}

Zadanie może działać w głównym wątku, gdy używasz synchronizacji w GCD. Synchronizacja uruchamia blok w danej kolejce i czeka na jego zakończenie, co skutkuje zablokowaniem głównego wątku lub głównej kolejki.Ponieważ główna kolejka musi czekać do zakończenia wysyłanego bloku, główny wątek będzie dostępny do przetwarzania bloków z kolejek innych niż Dlatego istnieje szansa, że ​​kod wykonywany w tle kolejka może faktycznie być wykonywana w głównym wątku. Ponieważ jest to kolejka szeregowa, wszystkie są wykonywane w kolejności ich dodawania (FIFO).

func doLongASyncTaskInConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.async {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executing")
    }
}

Zadanie będzie działało w tle w wątku, gdy użyjesz async w GCD. Async oznacza, że ​​wykonaj następną linię, nie czekaj do wykonania bloku, co skutkuje nieblokującym wątkiem głównym. Pamiętaj, że w kolejce współbieżnej zadania są przetwarzane w kolejności, w jakiej są dodawane do kolejki, ale z różnymi wątkami dołączonymi do kolejki. Pamiętaj, że nie mają one kończyć zadania w kolejności, w jakiej są dodawane do kolejki. Kolejność zadań różni się za każdym razem, gdy tworzone są wątki, ponieważ koniecznie są one automatycznie. Zadania wykonywane są równolegle. Z więcej niż tym (maxConcurrentOperationCount) zostanie osiągnięty, niektóre zadania będą zachowywać się jak szereg, dopóki wątek nie zostanie zwolniony.

func doLongSyncTaskInConcurrentQueue() {
  let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executed")
    }
}

Zadanie może działać w głównym wątku, gdy używasz synchronizacji w GCD. Synchronizacja uruchamia blok w danej kolejce i czeka na jego zakończenie, co skutkuje zablokowaniem głównego wątku lub głównej kolejki.Ponieważ główna kolejka musi czekać do zakończenia wysyłanego bloku, główny wątek będzie dostępny do przetwarzania bloków z kolejek innych niż Dlatego istnieje szansa, że ​​kod wykonywany w kolejce w tle może faktycznie być wykonywany w głównym wątku. Ze względu na kolejkę współbieżną zadania mogą nie kończyć się w kolejności, w jakiej są dodawane do kolejki. Ale w przypadku operacji synchronicznych tak jest, chociaż mogą być przetwarzane przez różne wątki. Tak więc zachowuje się tak, jakby to była kolejka szeregowa.

Oto podsumowanie tych eksperymentów

Pamiętaj, że korzystając z GCD, dodajesz zadanie tylko do kolejki i wykonujesz zadanie z tej kolejki. Kolejka wysyła Twoje zadanie w wątku głównym lub w tle, w zależności od tego, czy operacja jest synchroniczna czy asynchroniczna. Typy kolejek to Serial, Concurrent, Main dispatch queue.Wszystkie wykonywane zadania są wykonywane domyślnie z głównej kolejki wysyłkowej.Istnieją już cztery predefiniowane globalne kolejki współbieżne, z których może korzystać Twoja aplikacja i jedna główna kolejka (DispatchQueue.main). można również ręcznie utworzyć własną kolejkę i wykonać zadanie z tej kolejki.

Zadanie związane z interfejsem użytkownika powinno być zawsze wykonywane z głównego wątku poprzez wysłanie zadania do kolejki głównej DispatchQueue.main.sync/async podczas gdy operacje związane z siecią / ciężkie operacje powinny być zawsze wykonywane asynchronicznie, bez względu na to, w jakim wątku używasz głównego lub tła

EDYCJA: Jednak są przypadki, w których musisz wykonywać operacje wywołań sieciowych synchronicznie w wątku w tle bez zamrażania interfejsu użytkownika (np. Odświeżanie tokena OAuth i czekanie, czy się powiedzie, czy nie) .Musisz opakować tę metodę wewnątrz operacji asynchronicznej. operacje są wykonywane w kolejności i bez blokowania głównego wątku.

func doMultipleSyncTaskWithinAsynchronousOperation() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    concurrentQueue.async {
        let concurrentQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
        for i in 1...5 {
            concurrentQueue.sync {
                let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
                let _ = try! Data(contentsOf: imgURL)
                print("\(i) completed downloading")
            }
            print("\(i) executed")
        }
    }
}

EDIT EDIT: Można oglądać filmy demo tutaj

LC 웃
źródło
Świetna demonstracja ... następna linia nie czekaj, aż blok się wykona, co powoduje, że główny wątek nie blokuje, dlatego jeśli użyjesz punktów przerwania w wątku w tle, przeskoczy do tego, }ponieważ tak naprawdę nie jest wykonywany w tym momencie
Honey
@Ten leniwy facet z iOS 웃 Nadal nie rozumiem różnicy między asynchronicznym współbieżnym a asynchronicznym serialem. Jakie są konsekwencje używania obu. Obie działają w tle, nie zakłócając interfejsu użytkownika. Dlaczego miałbyś kiedykolwiek używać synchronizacji? Czy nie wszystkie kody są synchronizowane. jedna po drugiej?
eonista
1
@GitSyncApp możesz obejrzeć wideo tutaj
Anish Parajuli 웃
@ Ten leniwy facet z iOS 웃: dzięki za zrobienie tego. Opublikowałem na slack swift-lang. Byłoby 👌 Gdybyś mógł również utworzyć o DispatchGroup i DispatchWorkItem. : D
eonista
Przetestowałem twój ostatni, funkcja concurrentQueue.syncof doLongSyncTaskInConcurrentQueue(), wyświetla główny wątek, Task will run in different threadwydaje się nieprawda.
gabbler
54

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

bsod
źródło
7

Jeśli dobrze rozumiem, jak działa GCD, myślę, że są dwa rodzaje DispatchQueue, seriala concurrentjednocześnie są dwa sposoby DispatchQueuewysyłania jego zadań, przypisany closure, pierwszy asynci drugi sync. Te razem określają, w jaki sposób faktycznie wykonywane jest zamknięcie (zadanie).

Znalazłem to seriali mam na concurrentmyśli, ile wątków może wykorzystać kolejka, serialoznacza jeden, a concurrentoznacza wiele. A synci asyncoznacza to, że zadanie zostanie wykonane na który wątek, wątek rozmówcy lub gwint dotycząca tego kolejkę, syncśrodki uruchamiane na wątku wywołującego natomiast asyncśrodki uruchamiane na bazowym wątku.

Poniżej znajduje się kod eksperymentalny, który można uruchomić na placu zabaw Xcode.

PlaygroundPage.current.needsIndefiniteExecution = true
let cq = DispatchQueue(label: "concurrent.queue", attributes: .concurrent)
let cq2 = DispatchQueue(label: "concurent.queue2", attributes: .concurrent)
let sq = DispatchQueue(label: "serial.queue")

func codeFragment() {
  print("code Fragment begin")
  print("Task Thread:\(Thread.current.description)")
  let imgURL = URL(string: "http://stackoverflow.com/questions/24058336/how-do-i-run-asynchronous-callbacks-in-playground")!
  let _ = try! Data(contentsOf: imgURL)
  print("code Fragment completed")
}

func serialQueueSync() { sq.sync { codeFragment() } }
func serialQueueAsync() { sq.async { codeFragment() } }
func concurrentQueueSync() { cq2.sync { codeFragment() } }
func concurrentQueueAsync() { cq2.async { codeFragment() } }

func tasksExecution() {
  (1...5).forEach { (_) in
    /// Using an concurrent queue to simulate concurent task executions.
    cq.async {
      print("Caller Thread:\(Thread.current.description)")
      /// Serial Queue Async, tasks run serially, because only one thread that can be used by serial queue, the underlying thread of serial queue.
      //serialQueueAsync()
      /// Serial Queue Sync, tasks run serially, because only one thread that can be used by serial queue,one by one of the callers' threads.
      //serialQueueSync()
      /// Concurrent Queue Async, tasks run concurrently, because tasks can run on different underlying threads
      //concurrentQueueAsync()
      /// Concurrent Queue Sync, tasks run concurrently, because tasks can run on different callers' thread
      //concurrentQueueSync()
    }
  }
}
tasksExecution()

Mam nadzieję, że to może być pomocne.

Keith
źródło
7

Lubię to myśleć, używając tej metafory (tutaj jest link do oryginalnego obrazu):

Tata będzie potrzebował pomocy

Wyobraźmy sobie, że twój tata zmywa naczynia, a ty właśnie wypiłeś szklankę sody. Przynosisz szklankę tacie, żeby ją wyczyścił, i stawiasz ją obok drugiego naczynia.

Teraz twój tata zmywa naczynia sam, więc będzie musiał zmywać je jeden po drugim: Twój tata reprezentuje kolejkę szeregową .

Ale tak naprawdę nie jesteś zainteresowany staniem tam i patrzeniem, jak jest sprzątany. Więc upuszczasz szklankę i wracasz do swojego pokoju: nazywa się to wysyłaniem asynchronicznym . Twój tata może, ale nie musi, dać ci znać, kiedy skończy, ale ważne jest to, że nie czekasz, aż szkło zostanie wyczyszczone; wracasz do swojego pokoju, żeby robić, wiesz, dziecięce rzeczy.

Teraz załóżmy, że nadal jesteś spragniony i chcesz napić się wody na tej samej szklance, która jest twoją ulubioną, i naprawdę chcesz ją odzyskać, gdy tylko zostanie wyczyszczona. Więc stoisz tam i patrzysz, jak twój tata zmywa naczynia, dopóki twoje nie skończy. To jest wysłanie synchronizacji , ponieważ jesteś zablokowany, gdy czekasz na zakończenie zadania.

Na koniec powiedzmy, że twoja mama postanawia pomóc tacie i dołącza do niego zmywaniem. Teraz kolejka staje się kolejką współbieżną, ponieważ mogą jednocześnie czyścić wiele naczyń; ale pamiętaj, że nadal możesz zdecydować się tam poczekać lub wrócić do swojego pokoju, niezależnie od tego, jak działają.

Mam nadzieję że to pomoże

Yunus Nedim Mehel
źródło
3

1. Czytam, że kolejki szeregowe są tworzone i wykorzystywane do wykonywania zadań jedna po drugiej. Jednak co się stanie, jeśli: - • utworzę kolejkę szeregową • użyję dispatch_async (na właśnie utworzonej kolejce szeregowej) trzy razy do wysłania trzech bloków A, B, C

ODPOWIEDŹ : - Wszystkie trzy bloki wykonywane jeden po drugim Stworzyłem jeden przykładowy kod, który pomaga zrozumieć.

let serialQueue = DispatchQueue(label: "SampleSerialQueue")
//Block first
serialQueue.async {
    for i in 1...10{
        print("Serial - First operation",i)
    }
}

//Block second
serialQueue.async {
    for i in 1...10{
        print("Serial - Second operation",i)
    }
}
//Block Third
serialQueue.async {
    for i in 1...10{
        print("Serial - Third operation",i)
    }
}
CrazyPro007
źródło