Dlaczego nie możemy użyć synchronizacji wysyłek w bieżącej kolejce?

Natknąłem się na scenariusz, w którym miałem wywołanie zwrotne delegata, które mogło wystąpić zarówno w głównym wątku, jak i w innym wątku, i nie wiedziałem, który do czasu uruchomienia (używając StoreKit.framework).

Miałem również kod interfejsu użytkownika, który musiałem zaktualizować w tym wywołaniu zwrotnym, które musiało się wydarzyć przed wykonaniem funkcji, więc moja początkowa myśl była taka:

-(void) someDelegateCallback:(id) sender
{
    dispatch_sync(dispatch_get_main_queue(), ^{
        // ui update code here
    });

    // code here that depends upon the UI getting updated
}

To działa świetnie, gdy jest wykonywane na wątku tła. Jednak po wykonaniu na głównym thread, program wpada w impas.

To samo wydaje mi się interesujące, jeśli dobrze przeczytam dokumenty dla dispatch_sync, to spodziewam się, że po prostu uruchomi blok od razu, nie martwiąc się o zaplanowanie go w runloop, jak powiedział tutaj :

Jako optymalizacja, funkcja ta wywołuje blok na bieżącym wątku, gdy jest to możliwe.

Ale, to nie jest zbyt duża sprawa, to po prostu oznacza trochę więcej pisania, które prowadzą mnie do tego podejście:

-(void) someDelegateCallBack:(id) sender
{
    dispatch_block_t onMain = ^{
        // update UI code here
    };

    if (dispatch_get_current_queue() == dispatch_get_main_queue())
       onMain();
    else
       dispatch_sync(dispatch_get_main_queue(), onMain);
}

Jednak wydaje się to nieco wstecz. Czy to był błąd w tworzeniu GCD, czy jest coś, czego brakuje mi w dokumentach?

Author: Jano, 2012-06-11

6 answers

Znalazłem to w dokumentacji (ostatni rozdział):

Nie wywołaj funkcji dispatch_sync z wykonywanego zadania w tej samej kolejce, którą PRZEKAZUJESZ do wywołania funkcji. Zrobi to zablokować kolejkę. Jeśli chcesz wysłać do bieżącej kolejki, wykonaj asynchronicznie używając funkcji dispatch_async.

Również skorzystałem z linku, który podałeś i w opisie dispatch_sync przeczytałem to:

Wywołanie tej funkcji i kierowanie bieżącej kolejki powoduje impas.

Więc nie sądzę, że to problem z GCD, myślę, że jedynym sensownym podejściem jest to, które wymyśliłeś po odkryciu problemu.

 52
Author: lawicko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-12 14:02:19

dispatch_sync robi dwie rzeczy:

  1. Kolejka bloku
  2. blokuje bieżący wątek, dopóki blok nie zakończy działania

Biorąc pod uwagę, że główny wątek jest kolejką szeregową (co oznacza, że używa tylko jednego wątku), jeśli uruchomisz następującą instrukcję w kolejce głównej:

dispatch_sync(dispatch_get_main_queue(), ^(){/*...*/});

NastÄ ™ pujÄ ... ce wydarzenia:

  1. dispatch_sync ustawia blok w kolejce głównej.
  2. dispatch_sync blokuje wątek kolejki głównej aż do zakończenia bloku wykonuję.
  3. dispatch_sync czeka w nieskończoność, ponieważ wątek, w którym blok ma działać, jest zablokowany.

Kluczem do zrozumienia tego problemu jest to, że dispatch_sync nie wykonuje bloków, tylko ustawia je w kolejkach. Wykonanie nastąpi na przyszłej iteracji pętli run.

Następujące podejście:

if (queueA == dispatch_get_current_queue()){
    block();
} else {
    dispatch_sync(queueA, block);
}

Jest w porządku, ale pamiętaj, że nie ochroni Cię przed skomplikowanymi scenariuszami związanymi z hierarchią kolejek. W takim przypadku aktualna kolejka może być inna niż wcześniej zablokowana kolejka, w której próbujesz wysłać blok. Przykład:

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        // dispatch_get_current_queue() is B, but A is blocked, 
        // so a dispatch_sync(A,b) will deadlock.
        dispatch_sync(queueA, ^{
            // some task
        });
    });
});

W złożonych przypadkach Odczyt / Zapis danych klucz-wartość w kolejce wysyłania:

dispatch_queue_t workerQ = dispatch_queue_create("com.meh.sometask", NULL);
dispatch_queue_t funnelQ = dispatch_queue_create("com.meh.funnel", NULL);
dispatch_set_target_queue(workerQ,funnelQ);

static int kKey;
 
// saves string "funnel" in funnelQ
CFStringRef tag = CFSTR("funnel");
dispatch_queue_set_specific(funnelQ, 
                            &kKey,
                            (void*)tag,
                            (dispatch_function_t)CFRelease);

dispatch_sync(workerQ, ^{
    // is funnelQ in the hierarchy of workerQ?
    CFStringRef tag = dispatch_get_specific(&kKey);
    if (tag){
        dispatch_sync(funnelQ, ^{
            // some task
        });
    } else {
        // some task
    }
});

Wyjaśnienie:

  • tworzę workerQ kolejkę, która wskazuje na funnelQ kolejkę. W kodzie rzeczywistym jest to przydatne, jeśli masz kilka kolejek "worker" I chcesz wznowić / zawiesić wszystkie na raz(co jest osiągane przez wznowienie / aktualizację docelowej kolejki funnelQ).
  • mogę przekierowywać kolejki pracowników w dowolnym momencie, aby wiedzieć, czy są lejkowate, czy nie, oznaczam funnelQ słowem "lejek".
  • w dół drogi I dispatch_sync coś do workerQ, i z jakiegokolwiek powodu chcę dispatch_sync do funnelQ, ale unikając dispatch_sync do bieżącej kolejki, więc sprawdzam tag i odpowiednio działać. Ponieważ get wchodzi w hierarchię, wartość nie zostanie znaleziona w workerQ, ale zostanie znaleziona w funnelQ. Jest to sposób na sprawdzenie, czy jakakolwiek kolejka w hierarchii jest tą, w której przechowywaliśmy wartość. I dlatego, aby zapobiec dispatch_sync do bieżącej kolejki.

Jeśli zastanawiasz się nad funkcjami, które odczytują/zapisują dane kontekstowe, są trzy:

  • dispatch_queue_set_specific: napisz do kolejki.
  • dispatch_queue_get_specific: odczyt z kolejki.
  • dispatch_get_specific: Funkcja wygodna do odczytu z bieżącej kolejki.

Klucz jest porównywany przez wskaźnik i nigdy nie dereferowany. Ostatnim parametrem w setterze jest Destruktor zwalniający klucz.

Jeśli zastanawiasz się nad "wskazaniem jednej kolejki na drugą", to znaczy dokładnie to. Na przykład, mogę skierować kolejkę A do kolejki głównej, co spowoduje, że wszystkie bloki w kolejce a będą uruchamiane w kolejce głównej (zwykle odbywa się to w przypadku aktualizacji interfejsu).

 72
Author: Jano,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-12-07 12:54:43

Wiem skąd bierze się Twoje zamieszanie:

Jako optymalizacja, funkcja ta wywołuje blok na bieżącym wątek, jeśli to możliwe.

Ostrożnie, pisze aktualny wątek .

Nitka != Kolejka

Kolejka nie posiada wątku i wątek nie jest związany z kolejką. Są wątki i są kolejki. Gdy kolejka chce uruchomić blok, potrzebuje wątku, ale nie zawsze będzie to ten sam wątek. To po prostu potrzebuje do tego dowolnego wątku (za każdym razem może być inny), a gdy skończy się uruchamianie bloków (na razie), ten sam wątek może być teraz używany przez inną kolejkę.

Optymalizacja, o której mówi to zdanie, dotyczy wątków, a nie kolejek. Np. weź pod uwagę, że masz dwie kolejki szeregowe, QueueA i QueueB i teraz wykonaj następujące czynności:

dispatch_async(QueueA, ^{
    someFunctionA(...);
    dispatch_sync(QueueB, ^{
        someFunctionB(...);
    });
});

Kiedy QueueA uruchomi blok, będzie on tymczasowo właścicielem wątku, dowolnego wątku. someFunctionA(...) wykona w tym wątku. Teraz podczas robienia synchroniczna wysyłka, QueueA nie może zrobić nic innego, musi poczekać na zakończenie wysyłki. QueueB z drugiej strony, będzie również potrzebował wątku, aby uruchomić jego blok i wykonać someFunctionB(...). Więc albo QueueA tymczasowo zawiesza swój wątek i QueueB używa innego wątku do uruchomienia bloku, albo QueueA przekazuje swój wątek QueueB (w końcu i tak nie będzie go potrzebował, dopóki synchroniczna wysyłka nie zakończy się) i QueueB bezpośrednio używa bieżącego wątku QueueA.

Nie trzeba dodawać, że ostatni opcja jest znacznie szybsza, ponieważ nie jest wymagany przełącznik wątku. I to jest optymalizacją, o której mówi zdanie. Tak więc dispatch_sync() do innej kolejki może nie zawsze powodować przełączenie wątku (Inna kolejka, może ten sam wątek).

Ale dispatch_sync() nadal nie może się zdarzyć do tej samej kolejki (ten sam wątek, tak, ta sama kolejka, nie). Dzieje się tak dlatego, że kolejka wykonuje blok po bloku, a gdy aktualnie wykonuje blok, nie wykonuje kolejnego, dopóki nie zostanie wykonany aktualnie wykonywany. Więc to wykonuje BlockA i BlockA wykonuje dispatch_sync() z BlockB w tej samej kolejce. Kolejka nie będzie działać BlockB, dopóki nadal będzie działać BlockA, ale bieganie {[16] }nie będzie kontynuowane dopóki BlockB nie zostanie uruchomione. Widzisz problem? To klasyczny impas.

 16
Author: Mecki,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-24 10:17:13

Dokumentacja wyraźnie stwierdza, że przejście bieżącej kolejki spowoduje impas.

Teraz nie mówią, dlaczego projektowali rzeczy w ten sposób (poza tym, że faktycznie potrzeba dodatkowego kodu, aby to działało), ale podejrzewam, że powodem robienia rzeczy w ten sposób jest to, że w tym szczególnym przypadku, bloki byłyby "przeskakiwaniem" kolejki, tzn. w normalnych przypadkach Twój blok kończy się po uruchomieniu wszystkich innych bloków w kolejce, ale w tym przypadku byłby uruchomiony wcześniej.

To problem pojawia się, gdy próbujesz użyć GCD jako mechanizmu wzajemnego wykluczenia, a ten konkretny przypadek jest równoznaczny z użyciem rekurencyjnego mutex. Nie chcę wdawać się w spór o to, czy lepiej jest używać GCD lub tradycyjnego API wzajemnego wykluczania, takiego jak mutexy pthreads, czy nawet czy dobrym pomysłem jest używanie rekurencyjnych mutexów; pozwolę innym się spierać o to, ale z pewnością jest na to zapotrzebowanie, szczególnie gdy jest to główna kolejka, do której masz do czynienia z.

Osobiście uważam, że dispatch_sync byłby bardziej przydatny, gdyby obsługiwał tę funkcję lub gdyby istniała inna funkcja zapewniająca alternatywne zachowanie. Zachęcam innych, którzy tak myślą, aby złożyli zgłoszenie błędu w Apple (tak jak ja zrobiłem, ID: 12668073).

Możesz napisać własną funkcję, aby zrobić to samo, ale to trochę hack:

// Like dispatch_sync but works on current queue
static inline void dispatch_synchronized (dispatch_queue_t queue,
                                          dispatch_block_t block)
{
  dispatch_queue_set_specific (queue, queue, (void *)1, NULL);
  if (dispatch_get_specific (queue))
    block ();
  else
    dispatch_sync (queue, block);
}

N. B. wcześniej miałem przykład, który używał dispatch_get_current_queue (), ale teraz został wycofany.

 6
Author: Chris Suter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-03 21:14:33

Zarówno dispatch_async jak i {[1] } wykonują push swoją akcję na żądaną kolejkę. Akcja nie dzieje się natychmiast; dzieje się na jakiejś przyszłej iteracji pętli run kolejki. Różnica między dispatch_async a dispatch_sync polega na tym, że dispatch_sync blokuje bieżącą kolejkę do momentu zakończenia akcji.

Zastanów się, co się stanie, gdy wykonasz coś asynchronicznie w bieżącej kolejce. Ponownie, nie dzieje się to od razu, umieszcza go w kolejce FIFO i musi czekać aż po bieżąca iteracja pętli run jest wykonywana (i prawdopodobnie również czekać na inne akcje, które były w kolejce, zanim włączysz tę nową akcję).

Teraz możesz zapytać, kiedy wykonujesz akcję w bieżącej kolejce asynchronicznie, dlaczego nie zawsze po prostu wywołaj funkcję bezpośrednio, zamiast czekać do jakiegoś przyszłego czasu. Odpowiedź jest taka, że istnieje duża różnica między nimi. Wiele razy trzeba wykonać czynność, ale trzeba ją wykonać po niezależnie od strony efekty są wykonywane przez funkcje na stosie w bieżącej iteracji pętli run; lub musisz wykonać swoją akcję po jakiejś akcji animacji, która jest już zaplanowana na pętli run, itp. Dlatego często pojawia się kod [obj performSelector:selector withObject:foo afterDelay:0] (tak, różni się od [obj performSelector:selector withObject:foo]).

Jak już powiedzieliśmy, dispatch_sync jest tym samym co dispatch_async, z tym, że blokuje się do momentu zakończenia akcji. Jest więc oczywiste, dlaczego blok nie może wykonać się przynajmniej po bieżąca iteracja pętli run jest zakończona, ale czekamy na jej zakończenie przed kontynuacją.

Teoretycznie możliwe byłoby utworzenie specjalnego przypadku dla dispatch_sync, gdy jest to bieżący wątek, aby wykonać go natychmiast. (Taki specjalny przypadek istnieje dla performSelector:onThread:withObject:waitUntilDone:, gdy wątek jest bieżącym wątkiem i waitUntilDone: jest tak, to wykonuje go natychmiast.) Jednak wydaje mi się, że Apple uznało, że lepiej mieć tu konsekwentne zachowanie niezależnie od kolejki.

 4
Author: newacct,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-29 18:42:27

Znalezione z poniższej dokumentacji. https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//apple_ref/c/func/dispatch_sync

W przeciwieństwie do dispatch_async, "dispatch_sync " funkcja nie zwraca dopóki blok nie zostanie ukończony. Wywołanie tej funkcji i kierowanie bieżącej kolejki skutkuje zablokowaniem.

W przeciwieństwie do dispatch_async, nie jest wykonywane zachowanie w kolejce docelowej. Ponieważ wywołania tej funkcji są synchroniczne, to" pożycza " odniesienie do wywołującego. Co więcej, nie wykonuje się Block_copy na bloku.

Jako optymalizacja, funkcja ta wywołuje blok na bieżącym wątku, gdy jest to możliwe.

 2
Author: arango_86,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-04 06:22:38