Aguardando até que dois blocos assíncronos sejam executados antes de iniciar outro bloco


192

Ao usar o GCD, queremos esperar até que dois blocos assíncronos sejam executados e concluídos antes de passar para as próximas etapas de execução. Qual o melhor jeito pra fazer isso?

Tentamos o seguinte, mas parece não funcionar:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
});


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
});

// wait until both the block1 and block2 are done before start block3
// how to do that?

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
});

Veja minha resposta para o Swift 5, que oferece até seis maneiras diferentes de resolver seu problema.
Imanou Petit 23/03/19

Respostas:


301

Use grupos de despacho: veja aqui um exemplo, "Aguardando grupos de tarefas na fila" no capítulo "Filas de despacho" do Guia de programação de simultaneidade da biblioteca de desenvolvedores iOS da Apple

Seu exemplo pode ser algo como isto:

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block1
    NSLog(@"Block1");
    [NSThread sleepForTimeInterval:5.0];
    NSLog(@"Block1 End");
});


dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block2
    NSLog(@"Block2");
    [NSThread sleepForTimeInterval:8.0];
    NSLog(@"Block2 End");
});

dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
    // block3
    NSLog(@"Block3");
});

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group);

e poderia produzir uma saída como esta:

2012-08-11 16:10:18.049 Dispatch[11858:1e03] Block1
2012-08-11 16:10:18.052 Dispatch[11858:1d03] Block2
2012-08-11 16:10:23.051 Dispatch[11858:1e03] Block1 End
2012-08-11 16:10:26.053 Dispatch[11858:1d03] Block2 End
2012-08-11 16:10:26.054 Dispatch[11858:1d03] Block3

3
Legal. as tarefas / blocos assíncronos, uma vez associados ao grupo, serão executados sequencialmente ou simultaneamente? Quero dizer, suponha que o bloco1 e o bloco2 estejam associados a um grupo agora, o bloco2 esperará até que o bloco1 seja concluído antes de iniciar a execução?
Tom

9
Isso é contigo. dispatch_group_asyncé como dispatch_asynccom um parâmetro de grupo adicionado. Portanto, se você usar filas diferentes para o bloco1 e o bloco2 ou planejá-las na mesma fila simultânea, elas poderão ser executadas simultaneamente; se você agendá-los na mesma fila serial, eles serão executados em série. Não é diferente de agendar os blocos sem grupos.
Jörn Eyrich 11/08/2012

1
Isso também se aplica à execução de um serviço web?
precisa saber é o seguinte

Você percebe que o tempo não é igual ao tempo de sono definido no seu bloco? por que seria assim?
Damon Yuan

2
No ARC, remova o dispatch_release (group);
loretoparisi 28/09/2015

272

Expandindo a resposta de Jörn Eyrich (upvote a resposta dele se você aprovou esta), se você não tiver controle sobre as dispatch_asyncchamadas para seus blocos, como pode ser o caso dos blocos de conclusão assíncrona, você pode usar os grupos GCD usando dispatch_group_entere dispatch_group_leavediretamente.

Neste exemplo, estamos fingindo que computeInBackgroundé algo que não podemos mudar (imagine que seja um retorno de chamada delegado, NSURLConnection completeHandler ou qualquer outra coisa) e, portanto, não temos acesso às chamadas de despacho.

// create a group
dispatch_group_t group = dispatch_group_create();

// pair a dispatch_group_enter for each dispatch_group_leave
dispatch_group_enter(group);     // pair 1 enter
[self computeInBackground:1 completion:^{
    NSLog(@"1 done");
    dispatch_group_leave(group); // pair 1 leave
}];

// again... (and again...)
dispatch_group_enter(group);     // pair 2 enter
[self computeInBackground:2 completion:^{
    NSLog(@"2 done");
    dispatch_group_leave(group); // pair 2 leave
}];

// Next, setup the code to execute after all the paired enter/leave calls.
//
// Option 1: Get a notification on a block that will be scheduled on the specified queue:
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"finally!");
});

// Option 2: Block an wait for the calls to complete in code already running
// (as cbartel points out, be careful with running this on the main/UI queue!):
//
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // blocks current thread
// NSLog(@"finally!");

Neste exemplo, computeInBackground: conclusão: é implementado como:

- (void)computeInBackground:(int)no completion:(void (^)(void))block {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"%d starting", no);
        sleep(no*2);
        block();
    });
}

Saída (com registros de data e hora de uma execução):

12:57:02.574  2 starting
12:57:02.574  1 starting
12:57:04.590  1 done
12:57:06.590  2 done
12:57:06.591  finally!

1
@ obeuroburɳ O código acima aguarda no thread principal. Acredito que isso irá bloquear o segmento principal e fazer com que a interface do usuário não responda até que todo o grupo esteja completo. Eu recomendo mover a espera para um thread em segundo plano. Por exemplo, dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_HIGH, 0)
cbartel

2
@ cbartel, boa captura! Atualizei o código de exemplo para refletir seu comentário. Muitas vezes, você precisa que o retorno de chamada esteja na fila principal - nesse caso, o dispatch_queue_notifymelhor é provavelmente (a menos que o tempo de bloqueio seja curto).
ɲeuroburɳ

Onde posso liberar o grupo (por exemplo, dispatch_release (group))? Não tenho certeza se é seguro liberar em dispatch_group_notify. Mas como esse é o código que é executado após a conclusão do grupo, não tenho certeza de onde liberar.
GingerBreadMane

Se você estiver usando o ARC, não precisará chamar dispatch_release: stackoverflow.com/questions/8618632/…
--euroburɳ

3
Bom post que explica ainda mais: commandshift.co.uk/blog/2014/03/19/…
Rizon 31/08/2015

96

Com o Swift 5.1, o Grand Central Dispatch oferece várias maneiras de resolver seu problema. De acordo com suas necessidades, você pode escolher um dos sete padrões mostrados nos seguintes snippets do Playground.


# 1 Usando DispatchGroup, DispatchGroup's notify(qos:flags:queue:execute:)e DispatchQueue' sasync(group:qos:flags:execute:)

O Guia de Programação de Concorrência para Desenvolvedor da Apple declara sobreDispatchGroup :

Grupos de distribuição são uma maneira de bloquear um encadeamento até que uma ou mais tarefas concluam a execução. Você pode usar esse comportamento em locais onde não pode progredir até que todas as tarefas especificadas estejam concluídas. Por exemplo, após despachar várias tarefas para calcular alguns dados, você pode usar um grupo para aguardar essas tarefas e depois processar os resultados quando eles forem concluídos.

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

queue.async(group: group) {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async(group: group) {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

group.notify(queue: queue) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 2 Usando DispatchGroup, DispatchGroup's wait(), DispatchGroups enter()eDispatchGroup 'sleave()

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let group = DispatchGroup()

group.enter()
queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    group.leave()
}

group.enter()
queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    group.leave()
}

queue.async {
    group.wait()
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

Note que você também pode misturar DispatchGroup wait()com DispatchQueue async(group:qos:flags:execute:)ou misturar DispatchGroup enter()eDispatchGroup leave() com DispatchGroup notify(qos:flags:queue:execute:).


# 3 Usando eDispatch​Work​Item​Flags barrierDispatchQueue 'sasync(group:qos:flags:execute:)

Tutorial de Despacho da Grand Central para Swift 4: Parte 1/2 artigo da de Raywenderlich.com fornece uma definição para barreiras :

Barreiras de expedição são um grupo de funções que atuam como um gargalo no estilo de série ao trabalhar com filas simultâneas. Ao enviar um DispatchWorkItempara uma fila de despacho, você pode definir sinalizadores para indicar que esse deve ser o único item executado na fila especificada para esse horário específico. Isso significa que todos os itens enviados para a fila antes da barreira de expedição devem ser concluídos antes da DispatchWorkItemexecução.

Uso:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

queue.async(flags: .barrier) {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 4 Usando DispatchWorkItem, Dispatch​Work​Item​Flags's barriere DispatchQueue' sasync(execute:)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let dispatchWorkItem = DispatchWorkItem(qos: .default, flags: .barrier) {
    print("#3 finished")
}

queue.async(execute: dispatchWorkItem)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 5 Usando DispatchSemaphore, DispatchSemaphore's wait()e DispatchSemaphore' ssignal()

Soroush Khanlou escreveu as seguintes linhas na postagem do blog The GCD Handbook :

Usando um semáforo, podemos bloquear um thread por um período arbitrário de tempo, até que um sinal de outro thread seja enviado. Os semáforos, como o restante do GCD, são seguros para threads e podem ser acionados de qualquer lugar. Semáforos podem ser usados ​​quando há uma API assíncrona que você precisa tornar síncrona, mas não pode modificá-la.

O Apple Developer API Reference também fornece a seguinte discussão para DispatchSemaphore init(value:​) inicializador:

Passar zero para o valor é útil quando dois encadeamentos precisam reconciliar a conclusão de um evento específico. Passar um valor maior que zero é útil para gerenciar um conjunto finito de recursos, em que o tamanho do conjunto é igual ao valor.

Uso:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let queue = DispatchQueue(label: "com.company.app.queue", attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 0)

queue.async {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
    semaphore.signal()
}

queue.async {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
    semaphore.signal()
}

queue.async {
    semaphore.wait()
    semaphore.wait()    
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 */

# 6 Usando OperationQueuee Operation'saddDependency(_:)

A Referência da API do desenvolvedor da Apple declara sobre Operation​Queue :

As filas de operação usam o libdispatch biblioteca (também conhecida como Grand Central Dispatch) para iniciar a execução de suas operações.

Uso:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

let blockThree = BlockOperation {
    print("#3 finished")
}

blockThree.addDependency(blockOne)
blockThree.addDependency(blockTwo)

operationQueue.addOperations([blockThree, blockTwo, blockOne], waitUntilFinished: false)

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

# 7 Usando OperationQueuee OperationQueue's addBarrierBlock(_:)(requer iOS 13)

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()

let blockOne = BlockOperation {
    print("#1 started")
    Thread.sleep(forTimeInterval: 5)
    print("#1 finished")
}

let blockTwo = BlockOperation {
    print("#2 started")
    Thread.sleep(forTimeInterval: 2)
    print("#2 finished")
}

operationQueue.addOperations([blockTwo, blockOne], waitUntilFinished: false)
operationQueue.addBarrierBlock {
    print("#3 finished")
}

/*
 prints:
 #1 started
 #2 started
 #2 finished
 #1 finished
 #3 finished
 or
 #2 started
 #1 started
 #2 finished
 #1 finished
 #3 finished
 */

Existe uma solução para chamadas assíncronas sem usar group.enter () e group.leave () para cada um (e sem semáforos)? Como Se eu precisar aguardar uma solicitação assíncrona para um servidor, depois disso, aguarde uma segunda solicitação assíncrona e assim por diante. Eu li este artigo avanderlee.com/swift/asynchronous-operations, mas não vejo um uso simples dele em comparação ao BlockOperation
Woof

58

Outra alternativa da GCD é uma barreira:

dispatch_queue_t queue = dispatch_queue_create("com.company.app.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{ 
    NSLog(@"start one!\n");  
    sleep(4);  
    NSLog(@"end one!\n");
});

dispatch_async(queue, ^{  
    NSLog(@"start two!\n");  
    sleep(2);  
    NSLog(@"end two!\n"); 
});

dispatch_barrier_async(queue, ^{  
    NSLog(@"Hi, I'm the final block!\n");  
});

Basta criar uma fila simultânea, despachar seus dois blocos e, em seguida, despachar o bloco final com barreira, o que fará com que espere os outros dois terminarem.


Existe algum problema se eu não usei o sono (4);
Himanth 8/17/17

Não, é claro, não há problema com isso. Na verdade, você praticamente nunca quer sleep()! Eu apenas adicionei essas sleep()chamadas por razões pedagógicas, para fazer com que os blocos rodem o suficiente para que você possa ver que eles são executados simultaneamente. Neste exemplo trivial, na ausência de sleep(), esses dois blocos podem ser executados tão rapidamente que o bloco despachado pode começar e terminar antes que você tenha a chance de observar empiricamente a execução simultânea. Mas não sleep()no seu próprio código.
Rob

39

Eu sei que você perguntou sobre a GCD, mas se você quiser, NSOperationQueuetambém lida com esse tipo de coisa com muita elegância, por exemplo:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 3");
}];

NSOperation *operation;

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 1");
    sleep(7);
    NSLog(@"Finishing 1");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Starting 2");
    sleep(5);
    NSLog(@"Finishing 2");
}];

[completionOperation addDependency:operation];
[queue addOperation:operation];

[queue addOperation:completionOperation];

3
Isso é bom quando o código dentro do seu NSBlockOperation é síncrono. Mas e se não estiver, e você deseja acionar a conclusão quando sua operação assíncrona estiver concluída?
Greg Maletic,

3
@GregMaletic Nesse caso, faço uma NSOperationsubclasse que é simultânea e definida isFinishedquando o processo assíncrono é concluído. Então as dependências funcionam bem.
Rob


1
@GregMaletic Sim, você também pode usá-lo (desde que dispatch_semaphore_waitnão ocorra na fila principal e desde que seus sinais e esperas estejam equilibrados). Desde que você não bloqueie a fila principal, uma abordagem de semáforo é boa, se você não precisar da flexibilidade das operações (por exemplo, ter a capacidade de cancelá-las, capacidade de controlar o grau de simultaneidade, etc.).
Rob

1
@ Reza.Ab - Se você precisar concluir a tarefa um antes de iniciar a tarefa dois, adicione uma dependência entre essas tarefas. Ou, se a fila sempre estiver executando apenas uma tarefa por vez, torne-a uma fila serial configurando maxConcurrentOperationCountcomo 1. Você também pode definir a prioridade das operações, tanto a qualityOfServicecomo a queuePriority, mas elas têm um impacto muito mais sutil na prioridade da tarefa do que as dependências e / ou o grau de simultaneidade da fila.
21418 Rob

4

As respostas acima são legais, mas todas elas perderam uma coisa. O grupo executa tarefas (blocos) no encadeamento em que foi inserido quando você usa dispatch_group_enter/ dispatch_group_leave.

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      dispatch_async(demoQueue, ^{
        dispatch_group_t demoGroup = dispatch_group_create();
        for(int i = 0; i < 10; i++) {
          dispatch_group_enter(demoGroup);
          [self testMethod:i
                     block:^{
                       dispatch_group_leave(demoGroup);
                     }];
        }

        dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
          NSLog(@"All group tasks are done!");
        });
      });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

isso é executado na fila simultânea criada demoQueue . Se eu não criar nenhuma fila, ela será executada no thread principal .

- (IBAction)buttonAction:(id)sender {
    dispatch_group_t demoGroup = dispatch_group_create();
    for(int i = 0; i < 10; i++) {
      dispatch_group_enter(demoGroup);
      [self testMethod:i
                 block:^{
                   dispatch_group_leave(demoGroup);
                 }];
    }

    dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
      NSLog(@"All group tasks are done!");
    });
    }

    - (void)testMethod:(NSInteger)index block:(void(^)(void))completeBlock {
      NSLog(@"Group task started...%ld", index);
      NSLog(@"Current thread is %@ thread", [NSThread isMainThread] ? @"main" : @"not main");
      [NSThread sleepForTimeInterval:1.f];

      if(completeBlock) {
        completeBlock();
      }
    }

e há uma terceira maneira de fazer tarefas executadas em outro segmento:

- (IBAction)buttonAction:(id)sender {
      dispatch_queue_t demoQueue = dispatch_queue_create("com.demo.group", DISPATCH_QUEUE_CONCURRENT);
      //  dispatch_async(demoQueue, ^{
      __weak ViewController* weakSelf = self;
      dispatch_group_t demoGroup = dispatch_group_create();
      for(int i = 0; i < 10; i++) {
        dispatch_group_enter(demoGroup);
        dispatch_async(demoQueue, ^{
          [weakSelf testMethod:i
                         block:^{
                           dispatch_group_leave(demoGroup);
                         }];
        });
      }

      dispatch_group_notify(demoGroup, dispatch_get_main_queue(), ^{
        NSLog(@"All group tasks are done!");
      });
      //  });
    }

Obviamente, como mencionado, você pode usar dispatch_group_asyncpara obter o que deseja.


3

A primeira resposta está essencialmente correta, mas se você deseja a maneira mais simples de obter o resultado desejado, aqui está um exemplo de código independente demonstrando como fazê-lo com um semáforo (que também é como os grupos de despacho funcionam nos bastidores, JFYI) :

#include <dispatch/dispatch.h>
#include <stdio.h>

main()
{
        dispatch_queue_t myQ = dispatch_queue_create("my.conQ", DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t mySem = dispatch_semaphore_create(0);

        dispatch_async(myQ, ^{ printf("Hi I'm block one!\n"); sleep(2); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ printf("Hi I'm block two!\n"); sleep(4); dispatch_semaphore_signal(mySem);});
        dispatch_async(myQ, ^{ dispatch_semaphore_wait(mySem, DISPATCH_TIME_FOREVER); printf("Hi, I'm the final block!\n"); });
        dispatch_main();
}

7
Duas observações: 1. Você está perdendo a dispatch_semaphore_wait. Você tem dois sinais, então precisa de duas esperas. Como é, seu bloco de "conclusão" começará assim que o primeiro bloco sinalizar o semáforo, mas antes que o outro bloco termine; 2. Como essa era uma pergunta do iOS, eu desencorajaria o uso de dispatch_main.
Rob

1
Eu concordo com o Rob. Esta não é uma solução válida. O dispatch_semaphore_waitserá desbloqueado assim que um dos dispatch_semaphore_signalmétodos for chamado. A razão pela qual isso parece funcionar é que os printfblocos 'um' e 'dois' ocorrem imediatamente, e printfo 'finalmente' ocorre após uma espera - assim, depois que o bloco dorme por 2 segundos. Se você colocar o printf após as sleepchamadas, obterá a saída para 'um', depois 2 segundos para 'finalmente' e, em seguida, 2 segundos depois para 'dois'.
precisa saber é o seguinte

1

Resposta aceita rapidamente:

let group = DispatchGroup()

group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block1
    print("Block1")
    Thread.sleep(forTimeInterval: 5.0)
    print("Block1 End")
})


group.async(group: DispatchQueue.global(qos: .default), execute: {
    // block2
    print("Block2")
    Thread.sleep(forTimeInterval: 8.0)
    print("Block2 End")
})

dispatch_group_notify(group, DispatchQueue.global(qos: .default), {
    // block3
    print("Block3")
})

// only for non-ARC projects, handled automatically in ARC-enabled projects.
dispatch_release(group)

0

Exemplo do Swift 4.2:

let group = DispatchGroup.group(count: 2)
group.notify(queue: DispatchQueue.main) {
     self.renderingLine = false
     // all groups are done
}
DispatchQueue.main.async {
    self.renderTargetNode(floorPosition: targetPosition, animated: closedContour) {
        group.leave()
        // first done
    }
    self.renderCenterLine(position: targetPosition, animated: closedContour) {
        group.leave()
        // second done
    }
 }

group.leave()causou um acidente
Ben

-3

Para não dizer que outras respostas não são ótimas para determinadas circunstâncias, mas este é um trecho que eu sempre uso do Google:

- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {


    if (signInDoneSel) {
        [self performSelector:signInDoneSel];
    }

}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.