2014-05-25 11 views
7

Czytam komentarz do SO, że wysłanie kolejki do głównego wątku nie jest tym samym, co wykonanie kodu w głównym wątku. Jeśli dobrze zrozumiałem użytkownik mówił, że toJaka jest różnica między główną kolejką GCD a głównym wątkiem?

dispatch_async(dispatch_get_main_queue(), 
       ^{ 
         // some code 
       }); 

nie był taki sam jak ten

[self performSelectorOnMainThread:@selector(doStuff) 
         withObject:nil waitUntilDone:NO]; 

- (void) doStuff { 
    // some code 
} 

jest jakiś prawda o tym komentarzu?

Pomijając fakt, że pierwszy kod jest asynchroniczny, dla mnie oba kody są wykonywane jednakowo na głównym wątku. Czy istnieje między nimi różnica techniczna?

Pytam o to, ponieważ miałem trochę kodu, aby zaktualizować interfejs użytkownika za pomocą metody dispatch_async w głównym wątku i nie działało, ale kiedy zmieniłem to na drugi formularz przy użyciu performSelectorOnMainThread, zadziałało.

Odpowiedz

16

Tak, jest różnica. Główną kolejką wywołania jest kolejka szeregowa. Oznacza to, że podczas wykonywania zadania, które zostało mu przekazane, nie może wykonywać żadnych innych zadań. To prawda, nawet jeśli uruchamia wewnętrzną pętlę zdarzeń.

-performSelectorOnMainThread:... działa poprzez źródło pętli rozruchowej. Źródła działające w pętli mogą uruchamiać pętlę wewnątrz pętli, nawet jeśli ta wewnętrzna pętla uruchomieniowa jest wynikiem wcześniejszego uruchomienia tego samego źródła.

Jeden przypadek, w którym ten bit mnie polega na uruchomieniu otwartego okna dialogowego pliku modalnego. (Bez piaskownicy, więc okno dialogowe jest w trakcie.) Zainicjowałem dialog modalny z zadania przesłanego do głównej kolejki wysyłkowej. Okazuje się, że wewnętrzna implementacja otwartego okna dialogowego również asynchronicznie przekazuje niektóre prace do głównej kolejki. Ponieważ główna kolejka wywołania była zajęta przez moje zadanie, które uruchamiało to okno dialogowe, nie przetwarzała zadań tego frameworku aż do zakończenia okna dialogowego. Symptomem było to, że okno dialogowe nie wyświetliło plików, dopóki nie upłynął określony czas oczekiwania, który trwał mniej więcej minutę.

Należy zauważyć, że nie był to przypadek zakleszczenia spowodowanego przez synchroniczne żądaniez główną wątkiem, chociaż może się to również zdarzyć. W przypadku GCD takie synchroniczne żądanie jest pewnie zakleszczone. Z -performSelectorOnMainThread:..., nie będzie, ponieważ żądanie synchroniczne (waitUntilDone ustawione na YES) jest uruchamiane bezpośrednio.

Przy okazji, mówisz "pierwszy kod jest asynchroniczny", jakby chciał kontrastować z drugim kodem. Oba są asynchroniczne, ponieważ minąłeś NO dla waitUntilDone w drugim.


Aktualizacja:

Rozważmy kod tak:

dispatch_async(dispatch_get_main_queue(), ^{ 
    printf("outer task, milestone 1\n"); 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     printf("inner task\n"); 
    }); 
    // Although running the run loop directly like this is uncommon, this simulates what 
    // happens if you do something like run a modal dialog or call -[NSTask waitUntilExit]. 
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; 
    printf("outer task, milestone 2\n"); 
}); 

Będzie to log:

outer task, milestone 1 
outer task, milestone 2 
inner task 

Wewnętrzna zadanie nie będzie biec aż zewnętrzna zadanie ma zakończony. To prawda, nawet jeśli zewnętrzne zadanie uruchomiło pętlę głównego uruchomienia, która jest tym, co przetwarza zadania wysłane do głównej kolejki. Powodem jest to, że główna kolejka jest kolejką szeregową i nigdy nie uruchomi nowego zadania, dopóki nadal będzie wykonywać zadanie.

Jeśli zmienisz wewnętrzny wewnętrzny dispatch_async() na dispatch_sync(), program zostanie zablokowany.

Natomiast rozważenia:

- (void) task2 
{ 
    printf("task2\n"); 
} 

- (void) task1 
{ 
    printf("task1 milestone 1\n"); 
    [self performSelectorOnMainThread:@selector(task2) withObject:nil waitUntilDone:NO]; 
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; 
    printf("task1 milestone 2\n"); 
} 

(... in some other method:) 
    [self performSelectorOnMainThread:@selector(task1) withObject:nil waitUntilDone:NO]; 

To będzie log:

task1 milestone 1 
task2 
task1 milestone 2 

Running pętlę run wewnątrz od -task1 daje możliwość wewnętrznej -performSelectorOnMainThread:... do uruchomienia. To duża różnica między tymi dwoma technikami.

Jeśli zmienisz NO na YES w -task1, nadal działa bez impasu. To kolejna różnica. Dzieje się tak, ponieważ gdy -performSelectorOnMainThread:... jest wywoływana z waitUntilDone ustawiona na wartość true, sprawdza, czy jest wywoływana w głównym wątku. Jeśli tak, to po prostu bezpośrednio wywołuje wybierak właśnie tam. To tak, jakby to był tylko telefon do -performSelector:withObject:.

+0

* -performSelectorOnMainThread: ...działa poprzez źródło pętli rozruchowej. Źródła działające w pętli mogą uruchamiać pętlę wewnątrz pętli, nawet jeśli ta wewnętrzna pętla jest wynikiem wcześniejszego uruchomienia tego samego źródła. * Whaaaaaat? Nie można zrozumieć tego zwrotu. – SpaceDog

+0

To nie jest niemożliwe, wystarczy trochę zrozumieć o pętlach uruchamiania i źródłach pętli. Zobacz https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html –

+0

Zaktualizowałem moją odpowiedź przykładowym kodem. –

5

Tak, wydaje się, że występuje niewielka różnica. Załóżmy napisać kod i zobaczyć, co to jest:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { 
    NSLog(@"Starting!"); 

    CFRunLoopObserverRef o1 = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { 
     NSLog(@"runloop phase: %@", NSStringFromRunLoopActivity(activity)); 
    }); 
    CFRunLoopAddObserver(CFRunLoopGetMain(), o1, kCFRunLoopDefaultMode); 

    dispatch_async(dispatch_get_main_queue(), ^{ 
     NSLog(@"dispatch_async 1"); 
    }); 
    dispatch_async(dispatch_get_main_queue(), ^{ 
     NSLog(@"dispatch_async 2"); 
    }); 

    [self performSelectorOnMainThread:@selector(log) withObject:nil waitUntilDone:NO]; 
    [self performSelectorOnMainThread:@selector(log) withObject:nil waitUntilDone:NO]; 

    /* 
    NSLog(@"Reentering"); 
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; 
    NSLog(@"Reexiting"); 
    */ 

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 
     CFRunLoopRemoveObserver(CFRunLoopGetMain(), o1, kCFRunLoopDefaultMode); 
     CFRelease(o1); 
    }); 
} 

- (void)log { 
    NSLog(@"performSelector"); 
} 

W tym kawałku kodu, mamy zamiar założyć RunLoop Observer wstrzyknąć trochę rejestrowanie natomiast runloop kręci. Pomoże nam to zrozumieć, kiedy nasz asynchroniczny kod zostanie wykonany. (Funkcja NSStringFromRunLoopActivity() jest funkcją zwyczaju po prostu przekręcić wartość activity na ciąg znaków, a jego realizacja jest nieciekawe)

Jedziemy wysyłką dwie rzeczy do głównej kolejki i jedziemy do wysłania dwóch selektorów log do głównego wątku. Zauważ, że jesteśmy przed połączeniem dispatch_async.

Potem zrobimy przerwę obserwatora, więc nie dostaniemy kłótni.

Kiedy uruchomić to widzimy:

2014-05-25 07:57:26.054 EmptyAppKit[35437:303] Starting! 
2014-05-25 07:57:26.055 EmptyAppKit[35437:303] runloop phase: Entry 
2014-05-25 07:57:26.055 EmptyAppKit[35437:303] runloop phase: BeforeTimers 
2014-05-25 07:57:26.055 EmptyAppKit[35437:303] runloop phase: BeforeSources 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] performSelector 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] performSelector 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: Exit 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: Entry 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: BeforeTimers 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] runloop phase: BeforeSources 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] dispatch_async 1 
2014-05-25 07:57:26.056 EmptyAppKit[35437:303] dispatch_async 2 
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: Exit 
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: Entry 
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: BeforeTimers 
2014-05-25 07:57:26.057 EmptyAppKit[35437:303] runloop phase: BeforeSources 
2014-05-25 07:57:26.067 EmptyAppKit[35437:303] runloop phase: BeforeWaiting 
2014-05-25 07:57:26.068 EmptyAppKit[35437:303] runloop phase: AfterWaiting 
2014-05-25 07:57:26.068 EmptyAppKit[35437:303] runloop phase: Exit 
... 

Z tego widzę parę rzeczy:

  1. Run Loops wyjścia, jak tylko znalazłem coś do zrobienia. Zauważ, że w obu przypadkach, pętla startowa sprawdza źródła, ale nigdy nie przechodzi do etapu "BeforeWaiting", tak jak robi to później.
  2. Uruchomić pętle robią tyle rzeczy, ile mogą. W obu przypadkach pętla uruchamiania wykonuje zarównoperformSelectors, jak i obu dispatch_asyncs.
  3. Pętle uruchomione wolą wykonywać selektory overs wysyłając bloki. Pamiętasz, jak wysłaliśmy przed wykonaniem selektora? Jednak selektor został wykonany przed wykonaniem bloków. Zgaduję, że jakikolwiek mechanizm do wykonania tej pętli uruchomi wydajność selektora o wyższym priorytecie (lub po prostu wcześniej).
  4. Re-entrancy to nie zmienia. Jeśli odkomentujesz kod [[NSRunLoop mainRunLoop] runUntilDate...], kolejność rzeczy się nie zmieni, a oba bloki i selektory zostaną wykonane, ponownie wchodząc w pętlę uruchamiania.
+0

dlaczego # 1 się dzieje? – SpaceDog

+0

Myślę, że to wchodzi w chwasty, brakuje znacznie większych różnic. Chociaż # 4 jest poprawne w odniesieniu do tego przykładu, na ogół ponowne wprowadzenie pętli uruchamiania może wprowadzić dużą różnicę między tymi dwiema technikami. –

Powiązane problemy