Como espero que um bloco despachado de forma assíncrona termine?

180

Estou testando algum código que faz processamento assíncrono usando o Grand Central Dispatch. O código de teste fica assim:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

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

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

O que parece um pouco bruto, 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 muito o object.

zoul
fonte

Respostas:

302

Tentando usar a dispatch_semaphore. Deve ser algo como isto:

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

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

kperryua
fonte
61
Este código não funcionou para mim. Meu STAssert nunca seria executado. Eu tive que substituir o dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);comwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro
41
Provavelmente porque o seu bloco de conclusão é despachado para a fila principal? A fila é bloqueada aguardando o semáforo e, portanto, nunca executa o bloco. Veja esta pergunta sobre o envio na fila principal sem bloquear.
zoul
3
Segui a sugestão de @Zoul & nicktmro. Mas parece que está chegando ao estado de impasse. Caso de teste '- [BlockTestTest testAsync]' iniciado. mas nunca terminou
NSCry
3
Você precisa liberar o semáforo no ARC?
precisa saber é o seguinte
14
era exatamente isso que eu estava procurando. Obrigado! @ PeterWarbo não, você não. O uso do ARC elimina a necessidade de fazer um dispatch_release ()
Hulvej
29

Além da técnica do semáforo abordada exaustivamente em outras respostas, agora podemos usar o XCTest no Xcode 6 para executar testes assíncronos via XCTestExpectation. Isso elimina a necessidade de semáforos ao testar código assíncrono. 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 expedição 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íncronos, gravitando muito rapidamente em semáforos como um mecanismo geral para tornar assíncrona rotinas se comportam de forma síncrona. Pior, já vi muitos deles usarem essa técnica de semáforo na fila principal (e nunca devemos bloquear a fila principal nos aplicativos de produção).

Sei que esse não é o caso aqui (quando esta pergunta foi publicada, não havia uma ferramenta interessante como XCTestExpectation; também, nesses conjuntos de testes, devemos garantir que o teste não termine até que a chamada assíncrona seja concluída). Essa é uma daquelas situações raras em que a técnica de semáforo para bloquear o encadeamento principal pode ser necessária.

Portanto, com minhas desculpas ao autor desta pergunta original, para quem a técnica de semáforo é sólida, escrevo este aviso a todos os novos desenvolvedores que veem essa técnica de semáforo e considero aplicá-la em seu código como uma abordagem geral para lidar com assinaturas assíncronas. métodos: esteja avisado que nove em cada dez vezes, a técnica do semáforo não éa melhor abordagem ao contar operações assíncronas. Em vez disso, familiarize-se com os padrões de bloqueio / fechamento de conclusão, bem como com padrões e notificações de protocolo de delegação. 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, existem boas razões para as tarefas assíncronas terem sido projetadas para se comportarem de maneira assíncrona, portanto, use o padrão assíncrono correto, em vez de tentar fazê-las se comportar de maneira síncrona.

Roubar
fonte
1
Eu acho que essa deve ser a resposta aceita agora. Aqui estão os documentos também: developer.apple.com/library/prerelease/ios/documentation/…
hris.to 13/15
Eu tenho uma pergunta sobre isso. Eu tenho um código assíncrono que executa cerca de uma dúzia de chamadas de download do AFNetworking para baixar um único documento. Eu gostaria de agendar downloads em um NSOperationQueue. A menos que eu use algo como um semáforo, todos os downloads de documentos NSOperationaparecerão imediatamente completos e não haverá fila de downloads real - eles continuarão simultaneamente, o que eu não quero. Os semáforos são razoáveis ​​aqui? Ou existe uma maneira melhor de fazer as NSOperations aguardarem o fim assíncrono de outras pessoas? Ou alguma outra coisa?
21915 Benjohn
Não, não use semáforos nessa situação. Se você tiver uma fila de operações na qual está adicionando os AFHTTPRequestOperationobjetos, crie uma operação de conclusão (a qual dependerá das outras operações). Ou use grupos de expedição. BTW, você diz que não deseja que eles sejam executados simultaneamente, o que é bom se for o que você precisa, mas você paga uma séria penalidade de desempenho fazendo isso sequencialmente e não simultaneamente. Eu geralmente uso maxConcurrentOperationCount4 ou 5.
Rob
28

Recentemente, vim a esse problema novamente e escrevi a seguinte categoria em 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 chamadas assíncronas com retorno de chamada em síncronas nos testes:

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

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 a forma como o GCD se destina a funcionar e acabam causando conflitos e / ou morte da bateria por meio de pesquisas ininterruptas.

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 de alteração de estado (por exemplo, retornos de chamada / protocolos de delegados, disponibilidade, partida, erros, etc.). (Eles podem ser refatorados em blocos se você não gostar do inferno de retorno de chamada.) Como é assim, expor o comportamento real ao restante do aplicativo e ocultá-lo atrás de uma fachada falsa.

Em vez disso, use o NSNotificationCenter , defina um protocolo de delegação personalizado com retornos de chamada para sua classe. E se você não gosta de mexer com os retornos de chamada delegados, envolva-os em uma classe proxy concreta que implemente o protocolo personalizado e salve os vários blocos nas propriedades. Provavelmente também fornece construtores de conveniência também.

O trabalho inicial é um pouco mais alto, mas reduzirá o número de péssimas condições de corrida e pesquisas sobre bateria a longo prazo.

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


fonte
1
É um aviso importante por causa de padrões de projeto obj-C e capacidade de teste, também
bootmaker
8

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 o uso dispatch_synccom um bloco vazio para aguardar de forma síncrona em uma fila de expedição serial até que o bloco A-Synchronous seja concluído.

Leslie Godwin
fonte
O problema com esta resposta é que ele não resolve o problema original do OP, ou seja, a API que precisa ser usada usa um completeHandler como argumento e retorna imediatamente. Chamar essa API dentro do bloco assíncrono desta resposta retornaria imediatamente, mesmo que o completeHandler ainda não tivesse sido executado. Em seguida, o bloco de sincronização seria executado antes do
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);
}

Exemplo de uso:

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

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 objc.io para obter detalhes.) E como o Xcode 6 existe uma AsynchronousTestingcategoria XCTestque permite escrever códigos como este:

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

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);
Peter DeWeese
fonte
1
Há um erro no código acima. Na NSCondition documentação de -waitUntilDate:"Você deve bloquear o receptor antes de chamar este método". Então o -unlockdeve ser depois -waitUntilDate:.
3030 Patrick
Isso não é dimensionado para nada que use vários threads ou execute filas.
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]];
}

Isso fez por mim.


fonte
3
bem, ele causa alto uso de cpu embora
kevin
4
@ Kevin Yup, esta é a pesquisa do gueto que matará a bateria.
@ Barry, como consome mais bateria. por favor guia.
Pkc456
@ pkc456 Dê uma olhada em um livro de ciência da computação sobre as diferenças entre o funcionamento da pesquisa e da notificação assíncrona. Boa sorte.
2
Quatro anos e meio depois, e com o conhecimento e a experiência que adquiri, não recomendaria minha resposta.
0

Às vezes, os loops de tempo limite também são úteis. Você pode esperar até receber algum sinal (pode ser BOOL) do método de retorno de chamada assíncrono, mas e se não houver resposta alguma e quiser sair desse loop? Aqui abaixo está a solução, mais respondida acima, mas com uma adição 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];
}
Khulja Sim Sim
fonte
1
Mesmo problema: a vida útil da bateria falha.
1
@ Barry Não tenho certeza, mesmo se você olhou para o código. Há um período TIMEOUT_SECONDS dentro do qual, se a chamada assíncrona não responder, ela interromperá o loop. Esse é o truque para quebrar o impasse. Este código funciona perfeitamente sem matar a bateria.
Khulja Sim Sim
0

Solução muito primitiva para o problema:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

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

Swift 4:

Use em synchronousRemoteObjectProxyWithErrorHandlervez de remoteObjectProxyao criar o objeto remoto. Não há mais necessidade de um semáforo.

O exemplo abaixo retornará a versão recebida do proxy. Sem o synchronousRemoteObjectProxyWithErrorHandlertravamento (tentando acessar a memória não acessível):

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

Eu tenho que esperar até que um UIWebView seja carregado antes de executar meu método. Consegui fazê-lo executando verificações prontas do UIWebView no thread principal usando o GCD em combinação com os métodos de semáforo mencionados neste thread. O código final fica assim:

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