Jak czekać na zakończenie asynchronicznie wysłanego bloku?

180

Testuję kod, który wykonuje przetwarzanie asynchroniczne przy użyciu Grand Central Dispatch. Kod testowy wygląda następująco:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Testy muszą czekać na zakończenie operacji. Moje obecne rozwiązanie wygląda następująco:

__block BOOL finished = NO;
[object runSomeLongOperationAndDo:^{
    STAssert
    finished = YES;
}];
while (!finished);

Co wygląda nieco prymitywnie, znasz lepszy sposób? Mogę odsłonić kolejkę, a następnie zablokować, wywołując dispatch_sync:

[object runSomeLongOperationAndDo:^{
    STAssert
}];
dispatch_sync(object.queue, ^{});

… Ale to może wystawiać zbyt wiele na object.

zoul
źródło

Odpowiedzi:

302

Próbuję użyć dispatch_semaphore. Powinno to wyglądać mniej więcej tak:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert

    dispatch_semaphore_signal(sema);
}];

if (![NSThread isMainThread]) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
} else {
    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) { 
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; 
    }
}

Powinno to działać poprawnie, nawet jeśli runSomeLongOperationAndDo:uzna, że ​​operacja nie jest wystarczająco długa, aby zasłużyć na wątek, i zamiast tego działa synchronicznie.

kperryua
źródło
61
Ten kod nie działał dla mnie. Mój STAssert nigdy się nie uruchomi. Musiałem wymienić dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);zwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro
41
Prawdopodobnie dlatego, że Twój blok ukończenia jest wysyłany do głównej kolejki? Kolejka jest blokowana w oczekiwaniu na semafor i dlatego nigdy nie wykonuje bloku. Zobacz to pytanie dotyczące wysyłania w głównej kolejce bez blokowania.
zoul
3
Postępowałem zgodnie z sugestią @Zoul & nicktmro. Ale wygląda na to, że dojdzie do impasu. Test Case „- Uruchomiono [BlockTestTest testAsync]”. ale nigdy się nie skończyło
NSCry
3
Czy musisz zwolnić semafor w ramach ARC?
Peter Warbo,
14
właśnie tego szukałem. Dzięki! @PeterWarbo nie ty nie. Zastosowanie ARC eliminuje potrzebę wykonywania dispatch_release ()
Hulvej
29

Oprócz techniki semaforów omówionej wyczerpująco w innych odpowiedziach, możemy teraz używać XCTest w Xcode 6 do wykonywania testów asynchronicznych za pośrednictwem XCTestExpectation. Eliminuje to konieczność stosowania semaforów podczas testowania kodu asynchronicznego. Na przykład:

- (void)testDataTask
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];

    NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
    NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        XCTAssertNil(error, @"dataTaskWithURL error %@", error);

        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
            XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
        }

        XCTAssert(data, @"data nil");

        // do additional tests on the contents of the `data` object here, if you want

        // when all done, Fulfill the expectation

        [expectation fulfill];
    }];
    [task resume];

    [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

Ze względu na przyszłych czytelników, chociaż technika semafora wysyłki jest cudowną techniką, gdy jest absolutnie potrzebna, muszę przyznać, że widzę zbyt wielu nowych programistów, niezaznajomionych z dobrymi wzorcami programowania asynchronicznego, zbyt szybko przechodzą na semafory jako ogólny mechanizm tworzenia asynchronicznych procedury zachowują się synchronicznie. Co gorsza, wielu z nich korzysta z tej techniki semaforów z głównej kolejki (i nigdy nie powinniśmy blokować głównej kolejki w aplikacjach produkcyjnych).

Wiem, że tak nie jest w tym przypadku (kiedy to pytanie zostało opublikowane, nie było takiego fajnego narzędzia XCTestExpectation; również w tych zestawach testowych musimy upewnić się, że test nie zakończy się, dopóki nie zostanie wykonane wywołanie asynchroniczne). Jest to jedna z tych rzadkich sytuacji, w których może być konieczna technika semaforów do blokowania głównego wątku.

Dlatego przepraszam autora tego oryginalnego pytania, dla którego technika semafora jest dobra, piszę to ostrzeżenie do wszystkich nowych programistów, którzy widzą tę technikę semaforów i rozważają zastosowanie jej w kodzie jako ogólnego podejścia do radzenia sobie z asynchronicznym metody: Ostrzegamy, że dziewięć razy na dziesięć technika semaforów jest nienajlepsze podejście w przypadku operacji asynchronicznych. Zamiast tego zapoznaj się z wzorcami blokowania / zamykania, a także wzorcami i powiadomieniami protokołu delegowania. Są to często znacznie lepsze sposoby radzenia sobie z zadaniami asynchronicznymi, niż używanie semaforów, aby zachowywały się synchronicznie. Zwykle istnieją dobre powody, dla których zadania asynchroniczne zostały zaprojektowane tak, aby zachowywały się asynchronicznie, więc należy stosować właściwy wzorzec asynchroniczny zamiast próbować je synchronizować.

Obrabować
źródło
1
Myślę, że ta odpowiedź powinna być teraz zaakceptowana. Oto także dokumenty: developer.apple.com/library/prerelease/ios/documentation/…
hris.to
Mam pytanie na ten temat. Mam jakiś kod asynchroniczny, który wykonuje około tuzina wywołań pobierania w sieci AFNetworking, aby pobrać pojedynczy dokument. Chciałbym zaplanować pobieranie NSOperationQueue. O ile nie użyję czegoś w rodzaju semafora, wszystkie pobieranie dokumentów NSOperationnatychmiast się zakończy i nie będzie żadnego prawdziwego oczekiwania w kolejce do pobrania - będą postępować równolegle, czego nie chcę. Czy semafory są tutaj rozsądne? Czy jest lepszy sposób, aby NSOperations czekały na asynchroniczny koniec innych? Albo coś innego?
Benjohn
Nie, nie używaj semaforów w tej sytuacji. Jeśli masz kolejkę operacji, do której dodajesz AFHTTPRequestOperationobiekty, powinieneś po prostu utworzyć operację ukończenia (zależną od innych operacji). Lub użyj grup wysyłkowych. BTW, mówisz, że nie chcesz, aby działały równolegle, co jest w porządku, jeśli tego potrzebujesz, ale płacisz poważną karę wydajności wykonując to sekwencyjnie, a nie jednocześnie. Zwykle używam maxConcurrentOperationCount4 lub 5.
Rob
28

Niedawno ponownie dołączyłem do tego problemu i napisałem o następującej kategorii NSObject :

@implementation NSObject (Testing)

- (void) performSelector: (SEL) selector
    withBlockingCallback: (dispatch_block_t) block
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self performSelector:selector withObject:^{
        if (block) block();
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_release(semaphore);
}

@end

W ten sposób mogę łatwo przekształcić połączenie asynchroniczne z oddzwanianiem w połączenie synchroniczne w testach:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert
}];
zoul
źródło
24

Zasadniczo nie używaj żadnej z tych odpowiedzi, często się nie skalują (tu i tam są wyjątki, jasne)

Podejścia te są niezgodne ze sposobem, w jaki GCD ma działać, i doprowadzą do zablokowania i / lub zabicia baterii przez ciągłe odpytywanie.

Innymi słowy, należy zmienić kolejność kodu, tak aby nie było synchronicznego oczekiwania na wynik, ale zamiast tego radzić sobie z powiadomieniem wyniku o zmianie stanu (np. Wywołania zwrotne / protokoły delegowania, dostępność, odejście, błędy itp.). (Można je przekształcić w bloki, jeśli nie lubisz piekła zwrotnego.) Ponieważ w ten sposób możesz ujawnić realne zachowanie reszcie aplikacji, niż ukryć je za fałszywą fasadą.

Zamiast tego użyj NSNotificationCenter , zdefiniuj niestandardowy protokół delegowania z wywołaniami zwrotnymi dla swojej klasy. A jeśli nie lubisz tłumić wywołań zwrotnych delegatów, zawiń je w konkretną klasę proxy, która implementuje niestandardowy protokół i zapisuje różne właściwości bloku. Zapewne zapewnią również konstruktorów wygody.

Początkowa praca jest nieco większa, ale na dłuższą metę zmniejszy liczbę okropnych warunków wyścigowych i odpytywania o morderstwo baterii.

(Nie pytaj o przykład, ponieważ jest to trywialne i musieliśmy poświęcić czas na naukę podstaw celu c).


źródło
1
Jest to ważne ostrzeżenie ze względu na wzorce projektowe obj-C i testowalność
BootMaker
8

Oto sprytna sztuczka, która nie używa semafora:

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
    [object doSomething];
});
dispatch_sync(serialQ, ^{ });

To, co robisz, to czekanie przy użyciu dispatch_syncpustego bloku, aby Synchronicznie czekać w kolejce szeregowej wysyłki, aż do ukończenia bloku A-Synchronous.

Leslie Godwin
źródło
Problem z tą odpowiedzią polega na tym, że nie rozwiązuje ona pierwotnego problemu OP, a mianowicie, że interfejs API, który musi być użyty, przyjmuje argument handlowca uzupełniającego i natychmiast wraca. Wywołanie tego interfejsu API w bloku asynchronicznym tej odpowiedzi zostanie natychmiast zwrócone, nawet jeśli obiekt uzupełniania nie uruchomił się jeszcze. Następnie blok synchronizacji byłby wykonywany przed zakończeniem obsługi.
BTRUE
6
- (void)performAndWait:(void (^)(dispatch_semaphore_t semaphore))perform;
{
  NSParameterAssert(perform);
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  perform(semaphore);
  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  dispatch_release(semaphore);
}

Przykładowe użycie:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];
Oliver Atkinson
źródło
2

Istnieje również SenTestingKitAsync, który pozwala pisać kod w ten sposób:

- (void)testAdditionAsync {
    [Calculator add:2 to:2 block^(int result) {
        STAssertEquals(result, 4, nil);
        STSuccess();
    }];
    STFailAfter(2.0, @"Timeout");
}

(Szczegółowe informacje można znaleźć w artykule objc.io. ) A ponieważ Xcode 6 zawiera AsynchronousTestingkategorię, w XCTestktórej można pisać kod w ten sposób:

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];
zoul
źródło
1

Oto alternatywa dla jednego z moich testów:

__block BOOL success;
NSCondition *completed = NSCondition.new;
[completed lock];

STAssertNoThrow([self.client asyncSomethingWithCompletionHandler:^(id value) {
    success = value != nil;
    [completed lock];
    [completed signal];
    [completed unlock];
}], nil);    
[completed waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[completed unlock];
STAssertTrue(success, nil);
Peter DeWeese
źródło
1
W powyższym kodzie występuje błąd. Z NSCondition dokumentacji dla -waitUntilDate:„Musisz zablokować słuchawkę przed wywołaniem tej metody.” Tak -unlockpowinno być później -waitUntilDate:.
Patrick
Nie skaluje się do niczego, co używa wielu wątków lub kolejek uruchamiania.
0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object blockToExecute:^{
    // ... your code to execute
    dispatch_semaphore_signal(sema);
}];

while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
    [[NSRunLoop currentRunLoop]
        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}

Zrobiło to dla mnie.


źródło
3
cóż, powoduje to jednak wysokie użycie procesora
kevin
4
@kevin Yup, to sondaż w getcie, który zamorduje baterię.
@ Barry, w jaki sposób zużywa więcej baterii. proszę poprowadź.
pkc456
@ pkc456 Zapoznaj się z książką informatyki o różnicach między działaniem odpytywania a powiadomieniem asynchronicznym. Powodzenia.
2
Cztery i pół roku później, mając zdobytą wiedzę i doświadczenie, nie poleciłbym mojej odpowiedzi.
0

Czasami pomocne są również pętle Timeout. Czy możesz poczekać, aż pojawi się jakiś (być może BOOL) sygnał z asynchronicznej metody wywołania zwrotnego, ale co, jeśli nie będzie żadnej odpowiedzi i chcesz wyjść z tej pętli? Poniżej znajduje się rozwiązanie, na które najczęściej odpowiedziano powyżej, ale z dodatkiem Limit czasu.

#define CONNECTION_TIMEOUT_SECONDS      10.0
#define CONNECTION_CHECK_INTERVAL       1

NSTimer * timer;
BOOL timeout;

CCSensorRead * sensorRead ;

- (void)testSensorReadConnection
{
    [self startTimeoutTimer];

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {

        /* Either you get some signal from async callback or timeout, whichever occurs first will break the loop */
        if (sensorRead.isConnected || timeout)
            dispatch_semaphore_signal(sema);

        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:CONNECTION_CHECK_INTERVAL]];

    };

    [self stopTimeoutTimer];

    if (timeout)
        NSLog(@"No Sensor device found in %f seconds", CONNECTION_TIMEOUT_SECONDS);

}

-(void) startTimeoutTimer {

    timeout = NO;

    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:CONNECTION_TIMEOUT_SECONDS target:self selector:@selector(connectionTimeout) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void) stopTimeoutTimer {
    [timer invalidate];
    timer = nil;
}

-(void) connectionTimeout {
    timeout = YES;

    [self stopTimeoutTimer];
}
Khulja Sim Sim
źródło
1
Ten sam problem: awaria żywotności baterii.
1
@ Barry Nie jestem pewien, nawet jeśli spojrzałeś na kod. Jest TIMEOUT_SECONDS okres, w którym jeśli wywołanie asynchroniczne nie odpowie, przerwie pętlę. To jest hack, aby przełamać impas. Ten kod doskonale działa bez zabijania baterii.
Khulja Sim Sim
0

Bardzo prymitywne rozwiązanie problemu:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert
    nextOperationAfterLongOperationBlock();
}];
CAHbl463
źródło
0

Swift 4:

Użyj synchronousRemoteObjectProxyWithErrorHandlerzamiast remoteObjectProxypodczas tworzenia obiektu zdalnego. Koniec z semaforem.

Poniższy przykład zwróci wersję otrzymaną od proxy. Bez synchronousRemoteObjectProxyWithErrorHandlertego nastąpi awaria (próba dostępu do niedostępnej pamięci):

func getVersion(xpc: NSXPCConnection) -> String
{
    var version = ""
    if let helper = xpc.synchronousRemoteObjectProxyWithErrorHandler({ error in NSLog(error.localizedDescription) }) as? HelperProtocol
    {
        helper.getVersion(reply: {
            installedVersion in
            print("Helper: Installed Version => \(installedVersion)")
            version = installedVersion
        })
    }
    return version
}
Freek Sanders
źródło
-1

Muszę poczekać, aż UIWebView zostanie załadowany, zanim uruchomiłem moją metodę. Udało mi się to wykonać, wykonując kontrole gotowości UIWebView w głównym wątku za pomocą GCD w połączeniu z metodami semaforów wspomnianymi w tym wątku. Ostateczny kod wygląda następująco:

-(void)myMethod {

    if (![self isWebViewLoaded]) {

            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

            __block BOOL isWebViewLoaded = NO;

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                while (!isWebViewLoaded) {

                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        isWebViewLoaded = [self isWebViewLoaded];
                    });

                    [NSThread sleepForTimeInterval:0.1];//check again if it's loaded every 0.1s

                }

                dispatch_sync(dispatch_get_main_queue(), ^{
                    dispatch_semaphore_signal(semaphore);
                });

            });

            while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
            }

        }

    }

    //Run rest of method here after web view is loaded

}
Albert Renshaw
źródło