Como parar NSOperationQueue durante dispatch_async

Nov 03 2020

Estou adicionando muitas operações de bloco a uma fila de operações em um loop for. Em cada operação, preciso verificar em outro thread se uma condição foi atendida. Se a condição for atendida, todas as operações devem ser canceladas.

Fiz um código de amostra para mostrar meu problema:

__block BOOL queueDidCancel = NO;
NSArray *array = [NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10", nil];

NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.maxConcurrentOperationCount =1;


for (NSString *string in array) {
    [myQueue addOperationWithBlock:^{
        if (queueDidCancel) {return;}
        NSLog(@"run: %@", string);
        dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if ([string isEqualToString:@"1"]) {
                queueDidCancel = YES;
                [myQueue cancelAllOperations];
            }
        });
    }];
}

Saída esperada do NSLog:

run: 1

Resultado que recebi (varia entre 7 e 9):

run: 1
run: 2
run: 3
run: 4
run: 5
run: 6
run: 7
run: 8

Pesquisei horas no Google, mas não consegui encontrar uma solução.

Respostas

Chris Nov 04 2020 at 00:13

Acho que encontrei uma solução. Aqui está o código atualizado:

NSArray *array = [NSArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10", nil];

NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.maxConcurrentOperationCount =1;


for (NSString *string in array) {
    [myQueue addOperationWithBlock:^{
        [myQueue setSuspended:YES];
        NSLog(@"run: %@", string);
        dispatch_async(dispatch_get_main_queue(), ^{
            if (![string isEqualToString:@"1"]) {
                [myQueue setSuspended:NO];
            }
        });
    }];
}
skaak Nov 03 2020 at 23:28

Deixe-me usar mais espaço. Você precisa sincronizar o acesso à sua variável. É a ideia correta, mas você a está usando incorretamente. Você precisa de um bloqueio ou um ivar atômico ou algo parecido para sincronizar o acesso a ele.

Então, se você cancelar no bit dispatch_async, isso acontecerá muuuuito depois que todos os blocos forem executados. Isso é o que mostra sua saída. Conforme mencionado no comentário, se você adicionar um NSLog, por exemplo

dispatch_async(dispatch_get_main_queue(), ^{
            if ([string isEqualToString:@"1"]) {
                queueDidCancel = YES;
                // Add here
                NSLog(@"Going to cancel now");
                [myQueue cancelAllOperations];
            }

você vai ver o que quero dizer. Espero que normalmente execute profundamente em seu array ou mesmo depois que todo o array terminar de ser executado.

Mas o maior problema é a sua lógica. Você precisa de alguma lógica para cancelar esses blocos. Apenas enviar mensagens cancelAllOperationsou setSuspendednão é suficiente e os blocos que já estão rodando continuarão rodando.

Aqui está um exemplo rápido.

NSObject * lock = NSObject.new;      // Use this to sync access
__block BOOL queueDidCancel = NO;

NSOperationQueue *myQueue = [NSOperationQueue new];
myQueue.maxConcurrentOperationCount =1;

for (NSString *string in array) {
    // Here you also need to add some logic, e.g. as below
    // Note the sync access
    @synchronized ( lock ) {
      if (queueDidCancel) { break; }
    }

    [myQueue addOperationWithBlock:^{

        // You need to sync access to queueDidCancel especially if
        // you access it from main and the queue or if you increase
        // the concurrent count
        // This lock is one way of doing it, there are others
        @synchronized ( lock ) {
          // Here is your cancel logic! This is fine here
          if (queueDidCancel) {return;}
        }

        NSLog(@"run: %@", string);

        dispatch_async(dispatch_get_main_queue(), ^{

            if ([string isEqualToString:@"1"]) {
                // Again you need to sync this
                @synchronized ( lock ) {
                  queueDidCancel = YES;
                }
                // This is not needed your logic should take care of it ...
                // The problem is that running threads will probably
                // keep on running and you need logic to stop them
                // [myQueue cancelAllOperations];
            }
        });
    }];
}

Agora, este exemplo faz o que o seu faz, mas com um pouco mais de bloqueio e um pouco mais de lógica e NÃO cancelAllOperations nem suspendido = SIMs. Isso não fará o que você deseja, pois mesmo com essa execução, os threads tendem a ser executados até a conclusão e você precisa de lógica para interrompê-lo.

Além disso, neste exemplo, deixei a condição de saída ou cancelamento como está no thread principal. Novamente aqui, isso provavelmente significará que nada será cancelado, mas na vida real você normalmente cancelaria a partir de alguma IU, por exemplo, um clique de botão e então faria isso como aqui. Mas você pode cancelar em qualquer lugar usando a fechadura.

EDITAR

Com base em muitos comentários, aqui está outra maneira possível.

Aqui você verifica dentro do bloco e com base na verificação adiciona outro bloco ou não.

    NSOperationQueue * queue = NSOperationQueue.new;

    // Important
    queue.maxConcurrentOperationCount = 1;

    void ( ^ block ) ( void ) = ^ {

        // Whatever you have to do ... do it here
        xxx

        // Perform check
        // Note I run it sync and on the main queue, your requirements may differ
        dispatch_sync ( dispatch_get_main_queue (), ^ {

            // Here the condition is stop or not
            // YES means continue adding blocks
            if ( cond )
            {
                [queue addOperationWithBlock:block];
            }
            // else done

        } );

    };

    // Start it all
    [queue addOperationWithBlock:block];

Acima, uso o mesmo bloco todas as vezes, o que também é uma suposição, mas você pode alterá-lo facilmente para adicionar blocos diferentes. No entanto, se os blocos forem todos iguais, você só precisará de um e não precisará continuar agendando novos blocos e poderá fazê-lo conforme a seguir.

    void ( ^ block1 ) ( void ) = ^ {

        // Some logic
        __block BOOL done = NO;

        while ( ! done )
        {
            // Whatever you have to do ... do it here
            xxx

            // Perform check
            // Note I run it sync and on the main queue, your requirements may differ
            dispatch_sync ( dispatch_get_main_queue (), ^ {

                // Here the condition is stop or not
                // YES means stop! here
                done = cond;

            } );
        }

    };