Como parar NSOperationQueue durante dispatch_async
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
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];
}
});
}];
}
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;
} );
}
};