Cómo detener NSOperationQueue durante dispatch_async
Estoy agregando muchas operaciones de bloque a una cola de operaciones en un bucle for. En cada operación, necesito verificar en otro hilo si se cumple una condición. Si se cumple la condición, todas las operaciones deben cancelarse.
Hice un código de muestra para mostrarte mi 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];
}
});
}];
}
Resultado esperado de NSLog:
run: 1
Salida que obtuve (varía entre 7 y 9):
run: 1
run: 2
run: 3
run: 4
run: 5
run: 6
run: 7
run: 8
Busqué en Google durante horas, pero no pude encontrar una solución.
Respuestas
Creo que encontré una solución. Aquí está el código actualizado:
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];
}
});
}];
}
Déjame usar más espacio. Necesita sincronizar el acceso a su variable. Es la idea correcta pero la está utilizando incorrectamente. Necesita un candado o un ivar atómico o algo así para sincronizar el acceso a él.
Luego, si cancela en el bit dispatch_async, sucederá muuucho tiempo después de que se hayan ejecutado todos los bloques. Eso es lo que muestra su salida. Como se menciona en el comentario, si agrega un NSLog, por ejemplo
dispatch_async(dispatch_get_main_queue(), ^{
if ([string isEqualToString:@"1"]) {
queueDidCancel = YES;
// Add here
NSLog(@"Going to cancel now");
[myQueue cancelAllOperations];
}
verás lo que quiero decir. Espero que normalmente se ejecute profundamente en su matriz o incluso después de que toda la matriz haya terminado de ejecutarse.
Pero el mayor problema es tu lógica. Necesita algo de lógica para cancelar esos bloques. Solo enviar mensajes cancelAllOperationso setSuspendedno es suficiente y los bloques que ya se están ejecutando seguirán funcionando.
He aquí un ejemplo 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];
}
});
}];
}
Ahora, este ejemplo hace lo que hace el tuyo pero con un poco más de bloqueo y un poco más de lógica y NO cancelAllOperations ni suspension = YESs. Esto no hará lo que desea, ya que incluso con estos subprocesos en ejecución tienden a ejecutarse hasta completarse y necesita lógica para detenerlo.
Además, en este ejemplo, dejé la condición de salida o cancelación como está en el hilo principal. Nuevamente aquí, esto probablemente signifique que no se cancele nada, pero en la vida real normalmente cancelaría desde alguna interfaz de usuario, por ejemplo, al hacer clic en un botón y luego lo haría como aquí. Pero puedes cancelar en cualquier lugar usando el candado.
EDITAR
Basado en muchos comentarios, aquí hay otra forma posible.
Aquí verifica dentro del bloque y, según la verificación, agregue otro bloque o no.
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];
Arriba, utilizo el mismo bloque cada vez, lo que también es una suposición, pero puede cambiarlo fácilmente para agregar diferentes bloques. Sin embargo, si los bloques son todos iguales, solo necesitará uno y no es necesario que continúe programando nuevos bloques y luego puede hacerlo como se indica a continuación.
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;
} );
}
};