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

Testuję kod, który wykonuje asynchroniczne przetwarzanie za pomocą Grand Central Dispatch. Kod testowy wygląda tak:

[object runSomeLongOperationAndDo:^{
    STAssert…
}];

Testy muszą poczekać na zakończenie operacji. Moje obecne rozwiązanie wygląda tak:

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

Który wygląda trochę prymitywnie, znasz lepszy sposób? Mogę wyświetlić kolejkę, a następnie zablokować wywołując dispatch_sync:

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

...ale to może być zbyt wiele na object.

Author: skaffman, 2010-12-01

12 answers

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

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert…

    dispatch_semaphore_signal(sema);
}];

dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);

Powinno to zachowywać się poprawnie, nawet jeśli runSomeLongOperationAndDo: stwierdzi, że operacja nie jest wystarczająco długa, aby zasługiwać na wątek i działa synchronicznie.

 281
Author: kperryua,
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
2010-12-06 13:39:37

Oprócz techniki semafora opisanej wyczerpująco w innych odpowiedziach, możemy teraz używać XCTest w Xcode 6 do wykonywania testów asynchronicznych za pomocą 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, podczas gdy technika semafora dyspozytorskiego jest wspaniałą techniką, gdy jest absolutnie potrzebna, muszę przyznać, że widzę zbyt wielu nowych programistów, nieznających dobrych asynchronicznych wzorców programowania, zbyt szybko zbliżają się do semaforów, co jest ogólnym mechanizmem pozwalającym na zachowanie procedur asynchronicznych synchronicznie. Co gorsza, widziałem, że wielu z nich używa tej techniki semaforów z kolejki głównej (i nigdy nie powinniśmy blokować kolejki głównej w aplikacjach produkcyjnych).

Wiem, że tutaj tak nie jest (kiedy to pytanie zostało opublikowane, nie było ładnego narzędzia takiego jak XCTestExpectation; również w tych pakietach testowych musimy upewnić się, że test nie zakończy się, dopóki nie zostanie wykonane wywołanie asynchroniczne). To jeden z tych rzadkie sytuacje, w których technika semafora blokująca główny wątek może być konieczna.

Więc z przeprosinami dla autora tego oryginalnego pytania, dla którego technika semafora jest dźwiękowa, piszę to Ostrzeżenie dla wszystkich nowych programistów, którzy widzą tę technikę semafora i rozważają zastosowanie jej w swoim kodzie jako ogólnego podejścia do radzenia sobie z metodami asynchronicznymi: uprzedzamy, że dziewięć razy na dziesięć technika semafora jest , a nie najlepszym rozwiązaniem jest użycie semafora. podejście w przypadku napotkania operacji asynchronicznych. Zamiast tego zapoznaj się z wzorcami blokowania/zamykania zakończeń, a także wzorcami delegowania i powiadomieniami. Są to często o wiele lepsze sposoby radzenia sobie z zadaniami asynchronicznymi, zamiast używania semaforów, aby zachowywały się synchronicznie. Zwykle istnieją dobre powody, dla których zadania asynchroniczne zostały zaprojektowane tak, aby zachowywać się asynchronicznie, więc używaj WŁAŚCIWEGO wzorca asynchronicznego zamiast próbować je zachowywać synchronicznie.

 29
Author: Rob,
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-09-29 06:19:37

Ostatnio ponownie zająłem się tym zagadnieniem i napisałem następującą kategorię na 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 zamienić asynchroniczne połączenie z callback w synchroniczne w testach:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert…
}];
 27
Author: zoul,
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
2011-06-01 08:19:02

Generalnie nie używaj żadnej z tych odpowiedzi, często nie będą skalowane (są wyjątki tu i tam, oczywiście)

Te podejścia są niezgodne z tym, jak GCD jest przeznaczony do pracy i zakończy się albo powodując martwe punkty i / lub zabijanie baterii przez nonstop ankietowania.

Innymi słowy, Zmień swój kod tak, aby nie było synchronicznego oczekiwania na wynik, ale zamiast tego radzić sobie z wynikiem powiadamianym o zmianie stanu (np. dostępne, odejście, błędy itp.). (Można je przekształcić w bloki, jeśli nie lubisz callback hell.), Bo w ten sposób ujawnić realne zachowanie reszcie aplikacji, niż ukryć je za fałszywą fasadą.

Zamiast tego użyj NSNotificationCenter , zdefiniuj Niestandardowy protokół delegata z wywołaniami zwrotnymi dla twojej klasy. A jeśli nie lubisz muckingu z odwołaniami delegatów wszędzie, owiń je w konkretną klasę proxy, która implementuje Niestandardowy protokół i zapisuje różne blok we właściwościach. Prawdopodobnie również zapewniają wygodę konstruktorów.

Wstępna praca jest nieco większa, ale zmniejszy to liczbę okropnych warunków wyścigowych i morderczych sondaży na dłuższą metę.

(nie proś o przykład, ponieważ jest to banalne i musieliśmy zainwestować czas, aby nauczyć się podstaw objective-C.)

 21
Author: Barry,
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-09-06 02:11:53

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 czekasz używając dispatch_sync z pustym blokiem, aby synchronicznie czekać w kolejce wysyłania seryjnego, aż Blok A-synchronicznie się zakończy.

 8
Author: Leslie Godwin,
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
2016-04-22 04:55:44
- (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);
  }];
}];
 5
Author: Oliver Atkinson,
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-08-05 10:06:25

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

(Patrz objc.io Artykuł Po szczegóły.) A od Xcode 6 jest AsynchronousTesting Kategoria na XCTest, która pozwala 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];
 2
Author: zoul,
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-06-18 06:02:49

Oto alternatywa z 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);
 1
Author: Peter DeWeese,
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-05-30 12:39:29
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]];
}
To mi pomogło.
 0
Author: h0ussni,
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-01-26 21:23:17

Czasami pomocne są również pętle Timeout. Możesz poczekać, aż otrzymasz jakiś (może być BOOL) sygnał z metody asynchronicznego wywołania zwrotnego, ale co, jeśli nie ma odpowiedzi nigdy, i chcesz wyrwać się z tej pętli? Poniżej znajduje się rozwiązanie, Głównie powyżej, ale z dodatkiem Timeoutu.

#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];
}
 0
Author: Khulja Sim Sim,
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-04-18 02:26:59

Bardzo prymitywne rozwiązanie problemu:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert…
    nextOperationAfterLongOperationBlock();
}];
 0
Author: CAHbl463,
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
2018-03-05 13:54:58

Swift 4:

Użyj synchronousRemoteObjectProxyWithErrorHandler zamiast remoteObjectProxy podczas tworzenia zdalnego obiektu. Koniec z semaforem.

Poniższy przykład zwróci wersję otrzymaną z serwera proxy. Bez synchronousRemoteObjectProxyWithErrorHandler spowoduje awarię (próba uzyskania 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
}
 0
Author: Freek Sanders,
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
2018-09-17 07:29:29