49

Wpadłem na scenariusz, w którym miałem oddzwonienie do delegata, które mogło wystąpić w głównym wątku lub w innym wątku, i nie wiedziałbym, które z nich do czasu wykonania (używając StoreKit.framework).Dlaczego nie możemy użyć dispatch_sync w bieżącej kolejce?

miałem również kod UI, że potrzebne do aktualizacji w tym zwrotnego, które potrzebne wydarzy przed funkcją zrealizowana, więc moja pierwsza myśl była mieć funkcję tak:

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

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

że działa świetnie, kiedy jest wykonywany na wątku tła. Jednak po uruchomieniu na głównym wątku program przechodzi w stan zakleszczenia.

Że sam wydaje się interesujące dla mnie, jeśli czytam docs dispatch_sync prawo, a potem oczekiwać, że po prostu wykonać blok wprost, nie martwiąc się o zaplanowanie go do runloop, jak powiedział here:

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

jednak, że nie jest to zbyt wielkiego, oznacza to po prostu trochę więcej pisania, który doprowadzi mnie do tego podejścia:

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

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

+2

'dispatch_get_current_queue()' jest już przestarzałe. Sposobem na wykrycie głównej kolejki jest 'NSThread.isMainThread()' (Swift) lub [NSThread isMainThread] (Objective-C) – udondan

+0

'NSThread.isMainThread()' nie jest niezawodny, ponieważ w rzadkich przypadkach główne bloki kolejki, a GCD ponownie wykorzystuje główny wątek do wykonania innych kolejek. Zobacz [1] (http://blog.krzyzanowskim.com/2016/06/03/queues-are-not-bound-to-any-specific-thread/), [2] (http: //blog.benjamin -encz.de/post/main-queue-vs-main-thread/). – Jano

+0

@jtbandy należy uważać podczas oznaczania pytań jako duplikatów. To pytanie jest wyraźnie starsze i ma o wiele więcej aktywności niż to, które łączyłeś i być może powinny one być zamknięte w odwrotnym kierunku. –

Odpowiedz

44

Znalazłem to w the documentation (last chapter):

Nie wywołać funkcję dispatch_sync od zadania, które jest wykonującego w tej samej kolejce, aby przejść do wywołania funkcji. Spowoduje to zatrzymanie kolejki w postaci . Jeśli chcesz wysłać do bieżącej kolejki, wykonaj asynchronicznie za pomocą funkcji dispatch_async.

Ponadto, następnie link, który podałeś w opisie dispatch_sync czytam ten:

Wywołanie tej funkcji i kierowania aktualne wyniki kolejki w martwym punkcie.

Nie sądzę, że jest to problem z GCD, myślę, że jedynym sensownym podejściem jest to, które wymyśliliście po odkryciu problemu.

+0

Hmm. Zgaduję, co zamierzam zrobić, to zdefiniować makro, które wywołuje zachowanie, które moim zdaniem jest właściwe dla 'dispatch_sync'. –

+11

Muszę powiedzieć, że nie zgadzam się, że jest coś złego w działaniu ** dispatch_sync **. Jeśli się nad tym zastanowisz, zarówno ** dispatch_sync **, jak i ** async ** ustawiają w kolejce zadania, ale pierwsza również nie zwraca się, dopóki zadanie nie zostanie wykonane. W podanym przykładzie zadanie jest kolejkowane, ale nigdy nie wykonywane, i jest to bezpośrednia przyczyna impasu. Należy więc pamiętać, że główną funkcją tej funkcji jest faktyczne umieszczenie w kolejce zadania, a nie wywołanie go. Inwokacja to inna historia, ale z tego, co piszesz, wygląda na to, że ta funkcja faktycznie wywoła twoje zadanie. – lawicko

+7

Nie zgadzam się. Nie interesuje mnie to, jak 'dispatch_sync' działa pod okładkami, zależy mi na tym, aby z góry odgórnie wyglądało to, co robi,' wykonaj ten kod na danym wątku i wróć kiedy to zrobi '. Jeśli jestem na docelowym wątku, nie ma dla mnie żadnego sensu, aby sprawdzić, czy jestem na docelowym wątku, ponieważ funkcja powinna to zrobić dla mnie. Naprawdę mnie to zdziwiło, chociaż większość interfejsów API Apple'a jest mądrzejsza od tego, domyślam się, że deweloperzy po prostu byli leniwi w pracy? :) –

3

Zarówno dispatch_async, jak i dispatch_sync wykonują swoją akcję w żądanej kolejce. Akcja nie następuje natychmiast; dzieje się tak w przypadku przyszłej iteracji pętli uruchamiania kolejki. Różnica między dispatch_async i dispatch_sync polega na tym, że dispatch_sync blokuje bieżącą kolejkę, dopóki akcja się nie zakończy.

Zastanów się, co się stanie, gdy uruchomisz coś asynchronicznie w bieżącej kolejce. Znowu nie nastąpi to natychmiast; umieszcza go w kolejce FIFO i musi poczekać, aż zostanie wykonana aktualna iteracja pętli uruchamiania (i prawdopodobnie także czekać na inne akcje, które były w kolejce przed włączeniem tej nowej akcji).

Teraz możesz zapytać, podczas wykonywania akcji asynchronicznie w bieżącej kolejce, dlaczego nie zawsze po prostu wywołać funkcję bezpośrednio, zamiast czekać do pewnego czasu w przyszłości. Odpowiedź jest taka, że ​​między nimi jest duża różnica. Wiele razy, musisz wykonać akcję, ale musi być wykonana po niezależnie od efektów ubocznych są wykonywane przez funkcje na stosie w bieżącej iteracji pętli uruchamiania; lub musisz wykonać akcję po akcjach animacji, które są już zaplanowane w pętli uruchamiania itd. Dlatego wiele razy zobaczysz kod [obj performSelector:selector withObject:foo afterDelay:0] (tak, różni się od [obj performSelector:selector withObject:foo]).

Jak już wcześniej wspomnieliśmy, dispatch_sync jest takie samo jak dispatch_async, z tym wyjątkiem, że blokuje się, dopóki czynność nie zostanie zakończona. Jest więc oczywiste, dlaczego byłby zakleszczony - blok nie może zostać wykonany, dopóki przynajmniej nie zakończy się aktualna iteracja pętli uruchamiania; ale czekamy, aż się skończy, zanim przejdziemy dalej.

Teoretycznie byłoby możliwe wykonanie specjalnego przypadku dla dispatch_sync dla bieżącego wątku, aby wykonać go natychmiast. (Taki specjalny przypadek istnieje dla performSelector:onThread:withObject:waitUntilDone:, kiedy wątek jest bieżącym wątkiem, a waitUntilDone: jest TAK, wykonuje go natychmiast.Jednak sądzę, że Apple zdecydowało, że lepiej zachować spójne zachowanie, niezależnie od kolejki.

+0

Ale to nie ma sensu. Powinien istnieć przynajmniej komunikat logu wypisany na konsolę w przypadku pomyłki, tak jak ma to miejsce w przypadku innych API (np. Rekurencyjne 'NSLock'owanie). –

+0

@newacct "dispatch_sync blokuje bieżący wątek"? Zablokować bieżący wątek lub bieżącą kolejkę? – onmyway133

+0

@entropy: naprawiono – newacct

6

Dokumentacja wyraźnie stwierdza, że ​​przekazanie bieżącej kolejki spowoduje zakleszczenie.

Teraz nie mówią, dlaczego projektowali rzeczy w ten sposób (z wyjątkiem tego, że w rzeczywistości wymagałoby to 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 "przeskakuj" kolejkę, tzn. w normalnych przypadkach twój blok kończy działanie po tym, jak wszystkie inne bloki w kolejce zostały uruchomione, ale w tym przypadku działałby wcześniej.

Ten problem pojawia się, gdy próbujesz użyć GCD jako mechanizmu wzajemnego wykluczania, a ten konkretny przypadek jest równoważny użyciu muteksu rekursywnego. Nie chcę wdawać się w dyskusję o tym, czy lepiej jest używać GCD lub tradycyjnego API wykluczającego, takiego jak pthreads mutexes, czy nawet, czy dobrze jest używać muteksów rekursywnych; Pozwolę, żeby inni się spierali, ale na pewno jest na to popyt, szczególnie gdy jest to główna kolejka, z którą masz do czynienia.

Osobiście uważam, że dispatch_sync byłby bardziej przydatny, gdyby to działało lub gdyby istniała inna funkcja zapewniająca alternatywne zachowanie. Zachęcam innych, którzy myślą, że złożyć zgłoszenie błędu w Apple (tak jak ja to 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); 
} 

nb Wcześniej miałem przykład, który używał dispatch_get_current_queue(), ale teraz jest przestarzały.

+0

Zrobiłem podobne, z wyjątkiem makro, więc inny kod, który napisałem, który użył 'dispatch_sync' nie został złamany. +1 do ciebie! –

+1

Makro działa równie dobrze, ale ogólnie rzecz biorąc, radziłbym używać makra tylko wtedy, gdy nie można użyć statycznej funkcji inline, ponieważ są one z wielu powodów lepsze, a makra nie mają żadnych zalet. –

+1

dispatch_get_current_queue jest przestarzałe od iOS 6.x – openfrog

61

dispatch_sync robi dwie rzeczy:

  1. kolejkę bloku
  2. blokuje obecny wątek, aż blok zakończył prowadzenie

Zważywszy, że główny wątek jest kolejką seryjny (co oznacza używa tylko jednego wątku), następująca instrukcja:

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

powoduje następujące zdarzenia:

  1. dispatch_sync kolejek bloku w głównym kolejce.
  2. dispatch_sync blokuje wątek głównej kolejki, aż blok zostanie zakończony.
  3. dispatch_sync czeka na zawsze, ponieważ wątek, w którym blok ma działać, jest zablokowany.

Kluczem do zrozumienia tego jest to, że dispatch_sync nie wykonuje bloków, tylko je umieszcza w kolejce. Wykonanie nastąpi przy kolejnej iteracji pętli uruchamiania.

następujące podejście:

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

jest w porządku, ale należy pamiętać, że nie będzie cię chronić od złożonych scenariuszy obejmujących hierarchię kolejek. W takim przypadku bieżąca kolejka może być inna niż poprzednio zablokowana kolejka, w której próbujesz wysłać swój 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 
     }); 
    }); 
}); 

przypadku skomplikowanych przypadkach, odczyt/zapis danych klucz-wartość w kolejce wysyłkowy:

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

Objaśnienie:

  • tworzę workerQ kolejkę wskazujący na funnelQ kolejka. W prawdziwym kodzie jest to użyteczne, jeśli masz kilka kolejek "pracowniczych" i chcesz wznowić/zawiesić wszystkie naraz (co jest osiągane przez wznowienie/aktualizację kolejki docelowej).
  • Mogę kierować kolejkami pracowniczymi w dowolnym momencie, więc aby się dowiedzieć, czy są one przekazywane, czy nie, oznaczam tag funnelQ słowem "lejek".
  • dół drogi I dispatch_sync coś workerQ, i dla jakiegoś powodu chcę dispatch_sync do funnelQ, ale unikając dispatch_sync do bieżącej kolejki, więc sprawdzić dla tagu i podjąć odpowiednie działania. Ponieważ get idzie w górę hierarchii, wartość nie zostanie znaleziona w workerQ, ale zostanie znaleziona w funnelQ. Jest to sposób sprawdzenia, czy jakakolwiek kolejka w hierarchii jest tą, w której zapisaliśmy tę wartość. I dlatego, aby zapobiec dispatch_sync do bieżącej kolejki.

Jeśli zastanawiasz się o funkcjach, które odczytują dane/Context zapisu, są trzy:

  • dispatch_queue_set_specific: Napisz do kolejki.
  • dispatch_queue_get_specific: Czytaj z kolejki.
  • dispatch_get_specific: Wygodna funkcja do odczytu z bieżącej kolejki.

Klucz jest porównywany przez wskaźnik i nigdy nie jest dereferencjonowany. Ostatnim parametrem w ustawniku jest destruktor, który zwolni klucz.

Jeśli zastanawiasz się nad "wskazaniem jednej kolejki do drugiej", oznacza to dokładnie to. Na przykład mogę wskazać kolejkę A do głównej kolejki i spowoduje to, że wszystkie bloki w kolejce A będą działać w głównej kolejce (zwykle odbywa się to w przypadku aktualizacji interfejsu użytkownika).

+1

Oczywiście, że jest to poprawne. 'dispatch_sync' prawie nigdy nie jest do zrobienia, potrzebowałem go tylko kilka razy, aby zaktualizować i uzyskać wyniki z sekcji UI mojej aplikacji, po tym musisz wybrać coś innego. Twoja szalona technika sprawdzania hierarchii kolejki prawdopodobnie doprowadzi do bólu w drodze. –

+0

Jest zawiły i raczej mam wbudowaną 'amIChildOfQueue:', ale użycie kontekstu specyficznego dla kolejki jest rozwiązaniem zalecanym przez Apple w skomplikowanych przypadkach. Zobacz post # 6 w wątku [dispatch_get_current_queue() przestarzałe] (https://devforums.apple.com/message/710745). – Jano

+0

Czy możesz rzucić okiem na to pytanie? http://stackoverflow.com/questions/19833744/how-to-use-dispatch-queue-set-specific-and-dispatch-get-specific – hfossli

12

wiem gdzie zamieszanie pochodzi z:

Jako optymalizacji, funkcja ta wywołuje blok na obecnym wątku, jeśli to możliwe.

Ostrożnie, mówi bieżący wątek.

Wątek! = Kolejka

Kolejka nie posiada gwint i nitki nie jest zobowiązany do kolejki. Są wątki i są kolejki. Ilekroć kolejka chce uruchomić blok, potrzebuje wątku, ale to nie zawsze będzie ten sam wątek. Potrzebny jest tylko dowolny wątek (może to być za każdym razem inny), a kiedy zostanie uruchomiony blok (na razie), ten sam wątek może być teraz użyty przez inną kolejkę.

Optymalizacja tego zdania mówi o wątkach, a nie o kolejkach. Na przykład. rozważyć masz dwie kolejki seryjne QueueA i QueueB i teraz wykonać następujące czynności:

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

Kiedy QueueA uruchamia blok, będzie tymczasowo właścicielem nitkę, każdy wątek. someFunctionA(...) zostanie wykonane w tym wątku. Teraz podczas synchronicznej wysyłki, QueueA nie może zrobić nic innego, musi czekać na zakończenie wysyłki. QueueB z drugiej strony będzie również potrzebował wątku do uruchomienia swojego bloku i wykonania someFunctionB(...). Tak więc albo QueueA tymczasowo zawiesza wątek, a QueueB używa jakiegoś innego wątku do uruchomienia bloku lub QueueA przekazuje swój wątek do QueueB (w końcu i tak nie będzie go potrzebować do czasu zakończenia wysyłania synchronicznego) i QueueB bezpośrednio użyje bieżącego wątku QueueA.

Nie trzeba dodawać, że ostatnia opcja jest znacznie szybsza, ponieważ nie jest wymagany żaden przełącznik gwintu. I ta to optymalizacja, o której mówi zdanie. Zatem dispatch_sync() do innej kolejki nie zawsze może powodować przełączenie wątku (inna kolejka, być może ten sam wątek).

Ale nadal nie może się zdarzyć dispatch_sync() tej samej kolejce (ten sam wątek, tak, ta sama kolejka, nie).Dzieje się tak dlatego, że kolejka będzie wykonywała blok po bloku, a kiedy aktualnie wykonuje blok, nie wykona innego, dopóki ten nie zostanie ukończony. Tak więc wykonuje ona BlockA i z dispatch_sync() z w tej samej kolejce. Kolejka nie będzie działać BlockB, o ile nadal działa BlockA, ale uruchomienie BlockA nie będzie kontynuowane, dopóki nie zostanie uruchomione BlockB. Widzisz problem?

2

Znaleziono z następującej dokumentacji. https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//apple_ref/c/func/dispatch_sync

przeciwieństwie dispatch_async "dispatch_sync" funkcja nie zwraca aż blok zakończył. Wywołanie tej funkcji i skierowanie jej do bieżącej kolejki powoduje zakleszczenie.

W przeciwieństwie do dispatch_async, w kolejce docelowej nie jest wykonywane zatrzymanie. Ponieważ wywołania tej funkcji są synchroniczne, "pożycza" "odniesienie do osoby dzwoniącej. Ponadto w bloku nie jest wykonywana żadna operacja Block_copy.

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