Wie warte ich, bis ein asynchron versendeter Block fertig ist?

179

Ich teste Code, der asynchrone Verarbeitung mit Grand Central Dispatch ausführt. Der Testcode sieht folgendermaßen aus:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Die Tests müssen warten, bis der Vorgang abgeschlossen ist. Meine aktuelle Lösung sieht folgendermaßen aus:

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

Was ein bisschen grob aussieht, kennst du einen besseren Weg? Ich könnte die Warteschlange freigeben und dann blockieren, indem ich aufrufe dispatch_sync:

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

… Aber das macht vielleicht zu viel auf der object.

Zoul
quelle

Antworten:

302

Der Versuch, a dispatch_semaphore. Es sollte ungefähr so ​​aussehen:

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

Dies sollte sich auch dann korrekt verhalten, wenn runSomeLongOperationAndDo:entschieden wird, dass der Vorgang nicht lang genug ist, um ein Threading zu verdienen, und stattdessen synchron ausgeführt wird.

kperryua
quelle
61
Dieser Code hat bei mir nicht funktioniert. Mein STAssert würde niemals ausgeführt werden. Ich musste das dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);mitwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro
41
Das liegt wahrscheinlich daran, dass Ihr Abschlussblock an die Hauptwarteschlange gesendet wird? Die Warteschlange ist blockiert und wartet auf das Semaphor. Daher wird der Block niemals ausgeführt. Siehe diese Frage zum Versenden in der Hauptwarteschlange ohne Blockierung.
Zoul
3
Ich folgte dem Vorschlag von @Zoul & nicktmro. Aber es sieht so aus, als würde es zum Stillstand kommen. Testfall '- [BlockTestTest testAsync]' gestartet. aber nie beendet
NSCry
3
Müssen Sie das Semaphor unter ARC freigeben?
Peter Warbo
14
das war genau das, wonach ich gesucht habe. Vielen Dank! @ PeterWarbo nein du nicht. Die Verwendung von ARC beseitigt die Notwendigkeit, eine dispatch_release ()
Hulvej
29

Zusätzlich zu der Semaphorentechnik, die in anderen Antworten ausführlich behandelt wird, können wir jetzt XCTest in Xcode 6 verwenden, um asynchrone Tests über durchzuführen XCTestExpectation. Dadurch werden beim Testen von asynchronem Code keine Semaphoren benötigt. Beispielsweise:

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

Für zukünftige Leser ist die Versand-Semaphor-Technik zwar eine wunderbare Technik, wenn sie unbedingt benötigt wird, aber ich muss gestehen, dass zu viele neue Entwickler, die mit guten asynchronen Programmiermustern nicht vertraut sind, zu schnell zu Semaphoren als allgemeinem Mechanismus für die asynchrone Erstellung tendieren Routinen verhalten sich synchron. Schlimmer noch, ich habe gesehen, dass viele von ihnen diese Semaphor-Technik aus der Hauptwarteschlange verwenden (und wir sollten die Hauptwarteschlange in Produktions-Apps niemals blockieren).

Ich weiß, dass dies hier nicht der Fall ist (als diese Frage gestellt wurde, gab es kein nettes Tool wie XCTestExpectation; außerdem müssen wir in diesen Testsuiten sicherstellen, dass der Test erst beendet wird, wenn der asynchrone Aufruf abgeschlossen ist). Dies ist eine der seltenen Situationen, in denen die Semaphortechnik zum Blockieren des Hauptthreads erforderlich sein kann.

Mit meiner Entschuldigung an den Autor dieser ursprünglichen Frage, für den die Semaphortechnik fundiert ist, schreibe ich diese Warnung an alle neuen Entwickler, die diese Semaphortechnik sehen und in Betracht ziehen, sie in ihrem Code als allgemeinen Ansatz für den Umgang mit asynchronem Verhalten anzuwenden Methoden: Seien Sie gewarnt, dass neun von zehn die Semaphor-Technik nicht istDer beste Ansatz bei der Erfassung asynchroner Vorgänge. Machen Sie sich stattdessen mit Abschlussblock- / Abschlussmustern sowie mit Delegiertenprotokollmustern und Benachrichtigungen vertraut. Dies sind oft viel bessere Möglichkeiten, um mit asynchronen Aufgaben umzugehen, als Semaphoren zu verwenden, damit sie sich synchron verhalten. Normalerweise gibt es gute Gründe dafür, dass asynchrone Aufgaben so konzipiert sind, dass sie sich asynchron verhalten. Verwenden Sie daher das richtige asynchrone Muster, anstatt zu versuchen, sie synchron zu verhalten.

rauben
quelle
1
Ich denke, dies sollte jetzt die akzeptierte Antwort sein. Hier sind auch die Dokumente: developer.apple.com/library/prerelease/ios/documentation/…
hris.to
Ich habe eine Frage dazu. Ich habe einen asynchronen Code, der ungefähr ein Dutzend AFNetworking-Downloadaufrufe ausführt, um ein einzelnes Dokument herunterzuladen. Ich möchte Downloads auf einem planen NSOperationQueue. Wenn ich nicht so etwas wie ein Semaphor verwende, NSOperationwerden alle Dokumentendownloads sofort abgeschlossen und es wird keine echte Warteschlange für Downloads geben - sie werden so ziemlich gleichzeitig ablaufen, was ich nicht möchte. Sind Semaphoren hier sinnvoll? Oder gibt es eine bessere Möglichkeit, NSOperations auf das asynchrone Ende anderer warten zu lassen? Oder etwas anderes?
Benjohn
Nein, verwenden Sie in dieser Situation keine Semaphoren. Wenn Sie eine Operationswarteschlange haben, zu der Sie die AFHTTPRequestOperationObjekte hinzufügen , sollten Sie einfach eine Abschlussoperation erstellen (die Sie von den anderen Operationen abhängig machen). Oder verwenden Sie Versandgruppen. Übrigens, Sie sagen, Sie möchten nicht, dass sie gleichzeitig ausgeführt werden, was in Ordnung ist, wenn Sie dies benötigen, aber Sie zahlen ernsthafte Leistungseinbußen, wenn Sie dies nacheinander und nicht gleichzeitig tun. Ich benutze in maxConcurrentOperationCountder Regel 4 oder 5.
Rob
27

Ich bin kürzlich wieder zu diesem Thema gekommen und habe die folgende Kategorie geschrieben über 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

Auf diese Weise kann ich einen asynchronen Anruf mit einem Rückruf in Tests leicht in einen synchronen verwandeln:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert
}];
Zoul
quelle
24

Verwenden Sie im Allgemeinen keine dieser Antworten, sie werden oft nicht skaliert (es gibt hier und da Ausnahmen, klar)

Diese Ansätze sind nicht kompatibel mit der Funktionsweise von GCD und verursachen entweder Deadlocks und / oder töten die Batterie durch ununterbrochenes Abrufen.

Mit anderen Worten, ordnen Sie Ihren Code so um, dass nicht synchron auf ein Ergebnis gewartet wird, sondern dass ein Ergebnis über eine Statusänderung benachrichtigt wird (z. B. Rückrufe / Delegatenprotokolle, Verfügbarkeit, Weggehen, Fehler usw.). (Diese können in Blöcke umgewandelt werden, wenn Sie die Rückrufhölle nicht mögen.) Auf diese Weise können Sie dem Rest der App echtes Verhalten zeigen, anstatt es hinter einer falschen Fassade zu verbergen.

Verwenden Sie stattdessen NSNotificationCenter und definieren Sie ein benutzerdefiniertes Delegatenprotokoll mit Rückrufen für Ihre Klasse. Und wenn Sie nicht gerne überall mit delegierten Rückrufen herumspielen, schließen Sie sie in eine konkrete Proxy-Klasse ein, die das benutzerdefinierte Protokoll implementiert und die verschiedenen Blöcke in den Eigenschaften speichert. Wahrscheinlich bieten auch Convenience-Konstruktoren.

Die anfängliche Arbeit ist etwas mehr, aber sie wird auf lange Sicht die Anzahl der schrecklichen Rennbedingungen und der Umfragen zum Batteriemord verringern.

(Fragen Sie nicht nach einem Beispiel, da es trivial ist und wir die Zeit investieren mussten, um auch die objektiven Grundlagen zu erlernen.)


quelle
1
Es ist eine wichtige Warnung wegen obj-C Design-Mustern und Testbarkeit auch
BootMaker
8

Hier ist ein raffinierter Trick, bei dem kein Semaphor verwendet wird:

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

Warten Sie dispatch_syncmit einem leeren Block, um synchron auf eine serielle Versandwarteschlange zu warten, bis der A-Synchron-Block abgeschlossen ist.

Leslie Godwin
quelle
Das Problem bei dieser Antwort ist, dass das ursprüngliche Problem des OP nicht behoben wird. Die API, die verwendet werden muss, verwendet einen CompletionHandler als Argument und kehrt sofort zurück. Das Aufrufen dieser API innerhalb des asynchronen Blocks dieser Antwort würde sofort zurückkehren, obwohl der CompletionHandler noch nicht ausgeführt wurde. Dann würde der Synchronisierungsblock vor dem CompletionHandler ausgeführt.
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);
}

Anwendungsbeispiel:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];
Oliver Atkinson
quelle
2

Es gibt auch SenTestingKitAsync , mit dem Sie Code wie folgt schreiben können:

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

(Weitere Informationen finden Sie im Artikel objc.io. ) Und seit Xcode 6 gibt es eine AsynchronousTestingKategorie XCTest, in der Sie Code wie diesen schreiben können:

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];
Zoul
quelle
1

Hier ist eine Alternative zu einem meiner Tests:

__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
quelle
1
Im obigen Code ist ein Fehler aufgetreten. Aus der NSCondition Dokumentation zu -waitUntilDate:"Sie müssen den Empfänger sperren, bevor Sie diese Methode aufrufen." Also -unlocksollte das danach sein -waitUntilDate:.
Patrick
Dies lässt sich nicht auf etwas skalieren, das mehrere Threads verwendet oder Warteschlangen ausführt.
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]];
}

Das hat es für mich getan.


quelle
3
Nun
4
@ Kevin Yup, dies ist eine Ghetto-Umfrage, die die Batterie ermorden wird.
@ Barry, wie verbraucht es mehr Batterie. bitte führen.
pkc456
@ pkc456 Schauen Sie sich in einem Informatikbuch die Unterschiede zwischen der Funktionsweise von Abfragen und asynchronen Benachrichtigungen an. Viel Glück.
2
Viereinhalb Jahre später und mit dem Wissen und der Erfahrung, die ich gesammelt habe, würde ich meine Antwort nicht empfehlen.
0

Manchmal sind auch Timeout-Schleifen hilfreich. Können Sie warten, bis Sie ein (möglicherweise BOOL) Signal von der asynchronen Rückrufmethode erhalten, aber was ist, wenn Sie nie eine Antwort erhalten und aus dieser Schleife ausbrechen möchten? Hier unten finden Sie eine Lösung, die meistens oben beantwortet wurde, jedoch mit einem zusätzlichen Timeout.

#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
quelle
1
Gleiches Problem: Batterielebensdauer fällt aus.
1
@Barry Ich bin mir nicht sicher, ob du dir den Code angesehen hast. Es gibt einen Zeitraum von TIMEOUT_SECONDS, in dem die Schleife unterbrochen wird, wenn der asynchrone Aufruf nicht antwortet. Das ist der Hack, um die Sackgasse zu überwinden. Dieser Code funktioniert perfekt, ohne die Batterie zu beschädigen.
Khulja Sim Sim
0

Sehr primitive Lösung des Problems:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert
    nextOperationAfterLongOperationBlock();
}];
CAHbl463
quelle
0

Swift 4:

Verwenden Sie synchronousRemoteObjectProxyWithErrorHandlerstatt remoteObjectProxybeim Erstellen des Remote-Objekts. Kein Semaphor mehr nötig.

Das folgende Beispiel gibt die vom Proxy empfangene Version zurück. Ohne das synchronousRemoteObjectProxyWithErrorHandlerstürzt es ab (versucht auf nicht zugänglichen Speicher zuzugreifen):

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
quelle
-1

Ich muss warten, bis eine UIWebView geladen ist, bevor ich meine Methode ausführe. Ich konnte dies zum Laufen bringen, indem ich UIWebView-Ready-Checks für den Hauptthread mit GCD in Kombination mit den in diesem Thread erwähnten Semaphormethoden durchführte. Der endgültige Code sieht folgendermaßen aus:

-(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
quelle