Como posso esperar que um bloco despachado assincronamente termine?

Eu estou testando algum código que faz processamento asynchronous usando o Grand Central Dispatch. O código de teste é assim:

[object runSomeLongOperationAndDo:^{ STAssert… }]; 

Os testes devem aguardar a conclusão da operação. Minha solução atual é assim:

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

Que parece um pouco grosseiro, você conhece uma maneira melhor? Eu poderia expor a fila e depois bloquear chamando dispatch_sync :

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

… Mas isso talvez exponha demais no object .

Tentando usar um dispatch_sempahore . Deve ser algo como isto:

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

Isso deve se comportar corretamente, mesmo se runSomeLongOperationAndDo: decide que a operação não é realmente longa o suficiente para merecer o encadeamento e é executada de forma síncrona.

Além da técnica de semáforo coberta exaustivamente em outras respostas, agora podemos usar o XCTest no Xcode 6 para realizar testes asynchronouss via XCTestExpectation . Isso elimina a necessidade de semáforos ao testar o código asynchronous. Por exemplo:

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

Para o bem dos futuros leitores, embora a técnica de semáforo de envio seja uma técnica maravilhosa quando absolutamente necessária, devo confessar que vejo muitos desenvolvedores novos, não familiarizados com bons padrões de programação assíncrona, gravitando muito rapidamente para semáforos como um mecanismo geral para fazer assíncronas. rotinas se comportam de forma síncrona. Pior que eu tenha visto muitos deles usam essa técnica de semáforo da fila principal (e nunca devemos bloquear a fila principal em aplicativos de produção).

Eu sei que este não é o caso aqui (quando esta questão foi postada, não havia uma boa ferramenta como o XCTestExpectation ; também, nestes conjuntos de testes, nós devemos garantir que o teste não termine até que a chamada assíncrona seja feita). Essa é uma daquelas raras situações em que a técnica de semáforo para bloquear o thread principal pode ser necessária.

Então, com minhas desculpas ao autor desta pergunta original, para quem a técnica do semáforo é boa, eu escrevo este aviso para todos aqueles novos desenvolvedores que vêem essa técnica de semáforo e considerem aplicá-la em seu código como uma abordagem geral para lidar com asynchronous. methods: Esteja avisado que nove vezes em dez, a técnica de semáforo não é a melhor abordagem ao se montar operações assíncronas. Em vez disso, familiarize-se com os padrões de bloqueio / fechamento de conclusão, bem como padrões e notifications de protocolo delegado. Geralmente, essas são maneiras muito melhores de lidar com tarefas assíncronas, em vez de usar semáforos para fazê-las se comportar de maneira síncrona. Geralmente, há boas razões para as tarefas assíncronas terem sido projetadas para se comportarem de maneira assíncrona, portanto, use o padrão asynchronous correto em vez de tentar fazê-las se comportar de maneira síncrona.

Recentemente, cheguei a esse problema novamente e escrevi a seguinte categoria no 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 

Dessa forma, posso facilmente transformar uma chamada assíncrona com um retorno de chamada em um síncrono em testes:

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

Geralmente, não use nenhuma dessas respostas, elas geralmente não serão dimensionadas (há exceções aqui e ali, com certeza)

Essas abordagens são incompatíveis com o modo como o GCD deve funcionar e acabam causando deadlocks e / ou matando a bateria por meio de pesquisas sem parar.

Em outras palavras, reorganize seu código para que não haja espera síncrona por um resultado, mas lide com um resultado sendo notificado sobre a mudança de estado (por exemplo, retornos de chamada / protocolos delegates, disponibilidade, afastamento, erros, etc.). (Estes podem ser refatorados em blocos se você não gosta de callback hell.) Porque é como expor o comportamento real para o resto do aplicativo do que escondê-lo atrás de uma falsa fachada.

Em vez disso, use o NSNotificationCenter , defina um protocolo delegado personalizado com retornos de chamada para sua class. E, se você não gosta de usar o comando delegate callbacks, envolva-os em uma class proxy concreta que implemente o protocolo personalizado e salve os vários blocos nas propriedades. Provavelmente, também forneça construtores de conveniência.

O trabalho inicial é um pouco mais, mas reduzirá o número de terríveis condições de corrida e de assassinatos por bateria no longo prazo.

(Não peça um exemplo, porque é trivial e tivemos que investir tempo para aprender o básico do objective-c também.)

Aqui está um truque bacana que não usa um semáforo:

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

O que você faz é esperar usando dispatch_sync com um bloco vazio para Esperar sincronicamente em uma fila de envio serial até que o bloco A-Synchronous seja concluído.

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

Exemplo de uso:

 [self performAndWait:^(dispatch_semaphore_t semaphore) { [self someLongOperationWithSuccess:^{ dispatch_semaphore_signal(semaphore); }]; }]; 

Há também o SenTestingKitAsync que permite escrever código como este:

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

(Veja o artigo do objc.io para detalhes.) E desde o Xcode 6 há uma categoria AsynchronousTesting no XCTest que permite escrever código como este:

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

Aqui está uma alternativa de um dos meus testes:

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

Isso fez isso por mim.

Às vezes, os ciclos de tempo limite também são úteis. Você pode esperar até obter algum sinal (pode ser BOOL) do método de retorno de chamada asynchronous, mas e se nenhuma resposta, e você quiser sair desse loop? Aqui abaixo está a solução, a maioria respondida acima, mas com uma adição de Tempo Limite.

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

Solução muito primitiva para o problema:

 void (^nextOperationAfterLongOperationBlock)(void) = ^{ }; [object runSomeLongOperationAndDo:^{ STAssert… nextOperationAfterLongOperationBlock(); }];