Je teste un code qui effectue un traitement asynchrone à l'aide de Grand Central Dispatch. Le code de test ressemble à ceci:
[object runSomeLongOperationAndDo:^{
STAssert…
}];
Les tests doivent attendre la fin de l'opération. Ma solution actuelle ressemble à ceci:
__block BOOL finished = NO;
[object runSomeLongOperationAndDo:^{
STAssert…
finished = YES;
}];
while (!finished);
Ce qui semble un peu brutal, connaissez-vous un meilleur moyen? Je pourrais exposer la file d'attente puis bloquer en appelant dispatch_sync
:
[object runSomeLongOperationAndDo:^{
STAssert…
}];
dispatch_sync(object.queue, ^{});
… Mais c’est peut-être trop en exposer object
.
Essayer d'utiliser un dispatch_sempahore
. Ça devrait ressembler a quelque chose comme ca:
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);
Cela devrait se comporter correctement même si runSomeLongOperationAndDo:
décide que l'opération n'est pas suffisamment longue pour mériter un thread et s'exécute de manière synchrone.
En plus de la technique de sémaphore traitée de manière exhaustive dans d'autres réponses, nous pouvons maintenant utiliser XCTest dans Xcode 6 pour effectuer des tests asynchrones via XCTestExpectation
. Cela élimine le besoin de sémaphores lors du test de code asynchrone. Par exemple:
- (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];
}
Pour les futurs lecteurs, bien que la technique de sémaphore d’envoi soit une technique merveilleuse lorsqu’elle est absolument nécessaire, je dois avouer que je vois trop de nouveaux développeurs qui connaissent mal les bons modèles de programmation asynchrone, qu’il s’agit trop rapidement des sémaphores en tant que mécanisme général de création asynchrone. les routines se comportent de manière synchrone. Pire, j'ai vu beaucoup d'entre eux utiliser cette technique de sémaphore depuis la file d'attente principale (et nous ne devrions jamais bloquer la file d'attente principale dans les applications de production).
Je sais que ce n'est pas le cas ici (lorsque cette question a été posée, il n'y avait pas d'outil Nice comme XCTestExpectation
; de plus, dans ces suites de tests, nous devons nous assurer que le test ne se termine pas tant que l'appel asynchrone n'est pas terminé). C'est l'une des rares situations où la technique du sémaphore pour bloquer le thread principal peut être nécessaire.
Donc, avec mes excuses à l'auteur de cette question originale, pour qui la technique de sémaphore est valable, j'écris cet avertissement à tous ces nouveaux développeurs qui voient cette technique de sémaphore et envisagent de l'appliquer dans leur code comme une approche générale pour traiter les problèmes asynchrones. Méthodes: Soyez averti que neuf fois sur dix, la technique du sémaphore est not la meilleure approche lors de la réalisation d'opérations asynchrones. Au lieu de cela, familiarisez-vous avec les modèles de bloc/clôture d'achèvement, ainsi que les modèles et notifications de protocole de délégué. Ce sont souvent de bien meilleures façons de traiter des tâches asynchrones, plutôt que d'utiliser des sémaphores pour les faire se comporter de manière synchrone. Il y a généralement de bonnes raisons pour lesquelles les tâches asynchrones ont été conçues pour se comporter de manière asynchrone. Utilisez donc le bon modèle asynchrone plutôt que d'essayer de les faire se comporter de manière synchrone.
Je suis récemment revenu sur ce problème et j’ai écrit la catégorie suivante sur 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
De cette façon, je peux facilement transformer un appel asynchrone avec rappel en un appel synchrone lors des tests:
[testedObject performSelector:@selector(longAsyncOpWithCallback:)
withBlockingCallback:^{
STAssert…
}];
En général, n'utilisez aucune de ces réponses, elles ne seront souvent pas mises à l'échelle (il y a des exceptions ici et là, bien sûr)
Ces approches sont incompatibles avec la manière dont GCD est censé fonctionner et finissent par provoquer des blocages et/ou tuer la batterie par une scrutation continue.
En d’autres termes, réorganisez votre code de sorte qu’il n’y ait pas d’attente synchrone pour un résultat, mais que le résultat soit notifié en cas de changement d’état (par exemple, protocoles de rappel/délégation, disponibilité, retrait, erreurs, etc.). (Celles-ci peuvent être refactorisées en blocs si vous n'aimez pas le call-back enfer.) Parce que c'est ainsi que l'on expose le comportement réel au reste de l'application plutôt que de le cacher derrière une fausse façade.
À la place, utilisez NSNotificationCenter , définissez un protocole de délégué personnalisé avec des rappels pour votre classe. Et si vous n'aimez pas tout gâcher avec les rappels de délégués, enveloppez-les dans une classe proxy concrète qui implémente le protocole personnalisé et enregistre les différents blocs dans les propriétés. Probablement aussi fournir des constructeurs de commodité aussi.
Le travail initial est légèrement plus important, mais il permettra de réduire à long terme le nombre de conditions de course épouvantables et de scrutins meurtriers.
(Ne demandez pas d'exemple, car c'est trivial et nous avons également dû investir du temps pour apprendre les bases de Objective-C.)
Voici un truc astucieux qui n’utilise pas de sémaphore:
dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
[object doSomething];
});
dispatch_sync(serialQ, ^{ });
Ce que vous faites est d’attendre avec dispatch_sync
avec un bloc vide d’attendre de manière synchrone une file d’attente de distribution série jusqu’à ce que le bloc A-synchrone soit terminé.
- (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);
}
Exemple d'utilisation:
[self performAndWait:^(dispatch_semaphore_t semaphore) {
[self someLongOperationWithSuccess:^{
dispatch_semaphore_signal(semaphore);
}];
}];
Il y a aussi SenTestingKitAsync qui vous permet d’écrire du code comme celui-ci:
- (void)testAdditionAsync {
[Calculator add:2 to:2 block^(int result) {
STAssertEquals(result, 4, nil);
STSuccess();
}];
STFailAfter(2.0, @"Timeout");
}
(Voir article objc.io pour plus de détails.) Et depuis Xcode 6, il existe une catégorie AsynchronousTesting
sur XCTest
qui vous permet d’écrire du code comme celui-ci:
XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
[somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];
Voici une alternative à l'un de mes 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);
Swift 4:
Utilisez synchronousRemoteObjectProxyWithErrorHandler
au lieu de remoteObjectProxy
lors de la création de l'objet distant. Plus besoin d'un sémaphore.
L'exemple ci-dessous renverra la version reçue du proxy. Sans la synchronousRemoteObjectProxyWithErrorHandler
, cela plantera (en essayant d'accéder à de la mémoire non accessible):
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
}
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]];
}
Cela l'a fait pour moi.
Parfois, les boucles Timeout sont également utiles. Pouvez-vous attendre jusqu'à ce que vous obteniez un signal (peut-être BOOL) de la méthode de rappel asynchrone, mais que se passe-t-il si aucune réponse ne vous parvient, et si vous voulez sortir de cette boucle? ajout de 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];
}
Solution très primitive au problème:
void (^nextOperationAfterLongOperationBlock)(void) = ^{
};
[object runSomeLongOperationAndDo:^{
STAssert…
nextOperationAfterLongOperationBlock();
}];